Compare commits

...

68 Commits

Author SHA1 Message Date
mgsloan@gmail.com
4c43ec918e A little bit more progress 2024-12-13 00:00:44 -07:00
mgsloan@gmail.com
1e61989790 Progress! 2024-12-12 23:21:59 -07:00
Nathan Sobo
c3e5bc767a WIP: 177 errors left in workspace 2024-12-12 16:53:41 -07:00
Nathan Sobo
cf8c356d27 WIP 2024-12-12 16:00:14 -07:00
Nathan Sobo
3da7e7195e WIP 2024-12-12 15:29:33 -07:00
Nathan Sobo
10f88c841c WIP 2024-12-12 14:03:05 -07:00
Nathan Sobo
eb4f81abd8 WIP 2024-12-12 13:50:31 -07:00
Nathan Sobo
cd8fdeb15b WIP 2024-12-12 13:02:27 -07:00
Nathan Sobo
72d264063f WIP 2024-12-12 11:00:50 -07:00
Nathan Sobo
77e2816265 WIP 2024-12-12 09:46:42 -07:00
mgsloan@gmail.com
49cc0b8d68 Progress! 2024-12-12 04:08:08 -07:00
Nathan Sobo
7344dbbe2d Just kidding, workspace isn't compiling 2024-12-12 00:03:31 -07:00
Nathan Sobo
c345532126 WIP 2024-12-11 22:13:15 -07:00
Nathan Sobo
67870c8d53 The workspace crate compiles! 2024-12-11 22:08:01 -07:00
Nathan Sobo
edbb526401 WIP 2024-12-11 21:19:33 -07:00
Nathan Sobo
5987d8b78d WIP 2024-12-11 21:12:43 -07:00
Nathan Sobo
933157bae8 WIP: Now just workspace (more after that I know) 2024-12-11 18:06:23 -07:00
Nathan Sobo
d4dc607ac2 WIP: remote_server compiles 2024-12-11 18:00:03 -07:00
Nathan Sobo
fdb8e92178 WIP: Call crate compiles 2024-12-11 17:38:00 -07:00
Nathan Sobo
8d61c42a8f WIP: project crate compiles 2024-12-11 17:27:28 -07:00
Nathan Sobo
d0984600f7 WIP 2024-12-11 16:06:07 -07:00
Nathan Sobo
be776138b3 WIP 2024-12-11 09:00:41 -07:00
Nathan Sobo
0240f32099 WIP 2024-12-10 16:35:56 -07:00
Nathan Sobo
7ac00f30fa WIP 2024-12-10 09:59:25 -07:00
Nathan Sobo
282ad91336 WIP 2024-12-09 20:20:03 -07:00
Nathan Sobo
857000f535 WIP 2024-12-09 13:07:13 -07:00
Nathan Sobo
6d36e6f27a WIP: Remove ViewContext 2024-12-08 22:08:11 -07:00
Nathan Sobo
33fbfb83e5 WIP: Eliminate ModelContext usages 2024-12-08 21:34:34 -07:00
Nathan Sobo
299852cc7d Not compiling, removed WindowContext references 2024-12-08 21:19:06 -07:00
Nathan Sobo
7db32fbdf4 Merge remote-tracking branch 'origin/main' into gpui4 2024-12-08 13:54:54 -07:00
Nathan Sobo
54d59981da Remove *3 crates 2024-12-08 13:28:22 -07:00
Nathan Sobo
714af18afc Copy 3 versions of crates back to original 2024-12-08 13:24:33 -07:00
Nathan Sobo
906a2d9b9b Eliminate ModelContext 2024-12-08 13:21:42 -07:00
Nathan Sobo
c244416361 Compiling 2024-12-05 15:51:56 -07:00
Nathan Sobo
5607cf85c5 WIP 2024-12-05 09:58:48 -07:00
Nathan Sobo
b9250d08f9 Compiling with ui3 2024-12-04 19:49:40 -07:00
Nathan Sobo
6a5a9c507c WIP: Converting ui crate. 2 errors left 2024-12-04 19:46:29 -07:00
Nathan Sobo
1a90642088 WIP 2024-12-03 10:09:39 -07:00
Nathan Sobo
fe59d983eb Cleanup 2024-12-02 09:36:24 -07:00
Nathan Sobo
5ef8df22ac Compiling. 1 warning, no todos 2024-12-01 19:42:24 -07:00
Nathan Sobo
35407dcf8b WIP 2024-12-01 17:04:36 -07:00
Nathan Sobo
1ded8e4b8c WIP
- Updating input handling to use new Model-based architecture
- Refactoring TextInput and InputExample components
2024-12-01 16:59:28 -07:00
Nathan Sobo
dd32313cd0 Implement set_prompt_builder 2024-12-01 15:24:32 -07:00
Nathan Sobo
c55123afa6 Checkpoint 2024-12-01 15:19:41 -07:00
Nathan Sobo
d0cb78d09d Reintroduce Render trait as a requirement for window state 2024-12-01 15:17:24 -07:00
Nathan Sobo
1b82ba050f Reintroduce window state and typed window handles
This time however, the window's state is a model, not a special view.
You provide the window's state and a render function for that state.
2024-12-01 14:51:54 -07:00
Nathan Sobo
864fe2a306 Activate example app 2024-12-01 11:36:59 -07:00
Nathan Sobo
3d53f6da1d Checkpoint: Implement fallback prompt rendering 2024-12-01 09:47:43 -07:00
Nathan Sobo
d7e3b10776 Implement some more todo methods in window 2024-11-30 17:09:47 -07:00
Nathan Sobo
e6eeac8422 Implement some more todo methods in window 2024-11-30 15:22:26 -07:00
Nathan Sobo
6a705fda07 Implement some more todo methods in window 2024-11-30 15:16:54 -07:00
Nathan Sobo
76e6c7f6e3 Implement display 2024-11-30 12:58:45 -07:00
Nathan Sobo
1491ca7dcb Implement appearance_changed 2024-11-30 12:53:07 -07:00
Nathan Sobo
c842988c15 Implement bounds changed 2024-11-30 12:51:35 -07:00
Nathan Sobo
f33adc1a7a Compiling with lots of todos 2024-11-30 12:47:46 -07:00
Nathan Sobo
ad50231829 Accept builder functions in tooltip methods
Before the took render funcitons, but I'm not ready to fight that.
2024-11-30 12:09:26 -07:00
Nathan Sobo
fc152eb459 WIP: Work on rendering tooltips with functions (instead of views) 2024-11-25 19:52:24 -07:00
Nathan Sobo
c404896a32 WIP: Return a render fn from drag listener 2024-11-25 11:41:14 -07:00
Nathan Sobo
34d5f264b4 WIP: Progress toward eliminating window context, view context, views 2024-11-24 23:20:06 -07:00
Nathan Sobo
cf818948dd WIP 2024-11-24 22:41:00 -07:00
Nathan Sobo
1122e106a9 WIP 2024-11-24 22:39:08 -07:00
Nathan Sobo
cb729c4c7f WIP 2024-11-24 22:22:40 -07:00
Nathan Sobo
e28104b2e8 WIP 2024-11-24 22:19:50 -07:00
Nathan Sobo
a7159de184 Checkpoint 2024-11-24 19:43:15 -07:00
Nathan Sobo
3144490fac Change update_window to take a window reference 2024-11-24 19:18:13 -07:00
Nathan Sobo
561660ac07 Compiling with render functions (but lots of todos) 2024-11-24 16:26:35 -07:00
Nathan Sobo
d4d6be3f8d WIP: Give windows a render function instead of a root view 2024-11-24 14:12:23 -07:00
Nathan Sobo
368ff811b9 Clone gpui3 to gpui so I can work on it 2024-11-24 08:38:39 -07:00
475 changed files with 37203 additions and 27691 deletions

View File

@@ -3,9 +3,9 @@ use editor::Editor;
use extension_host::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
actions, percentage, Animation, AnimationExt as _, AppContext, AppContext, CursorStyle,
EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, VisualContext as _,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
@@ -46,34 +46,38 @@ struct PendingWork<'a> {
struct Content {
icon: Option<gpui::AnyElement>,
message: String,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
on_click:
Option<Arc<dyn Fn(&mut ActivityIndicator, &Model<ActivityIndicator>, &mut AppContext)>>,
}
impl ActivityIndicator {
pub fn new(
workspace: &mut Workspace,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
) -> View<ActivityIndicator> {
model: &Model<Workspace>,
cx: &mut AppContext,
) -> Model<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(|this, mut cx| async move {
while let Some((name, status)) = status_events.next().await {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
model
.spawn(cx, |this, mut cx| async move {
while let Some((name, status)) = status_events.next().await {
this.update(&mut cx, |this, model, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
model.notify(cx);
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| model.notify(cx)).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
cx.observe(auto_updater, |_, _, cx| model.notify(cx))
.detach();
}
Self {
@@ -86,7 +90,8 @@ impl ActivityIndicator {
cx.subscribe(&this, move |_, _, event, cx| match event {
Event::ShowError { lsp_name, error } => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let create_buffer =
project.update(cx, |project, model, cx| project.create_buffer(model, cx));
let project = project.clone();
let error = error.clone();
let lsp_name = lsp_name.clone();
@@ -105,8 +110,8 @@ impl ActivityIndicator {
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| {
Editor::for_buffer(buffer, Some(project.clone()), cx)
Box::new(cx.new_model(|model, cx| {
Editor::for_buffer(buffer, Some(project.clone()), model, cx)
})),
None,
true,
@@ -123,29 +128,44 @@ impl ActivityIndicator {
this
}
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
fn show_error_message(
&mut self,
_: &ShowErrorMessage,
model: &Model<Self>,
window: &mut Window,
cx: &mut AppContext,
) {
self.statuses.retain(|status| {
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
cx.emit(Event::ShowError {
lsp_name: status.name.clone(),
error: error.clone(),
});
model.emit(
cx,
Event::ShowError {
lsp_name: status.name.clone(),
error: error.clone(),
},
);
false
} else {
true
}
});
cx.notify();
model.notify(cx);
}
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
fn dismiss_error_message(
&mut self,
_: &DismissErrorMessage,
model: &Model<Self>,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
updater.update(cx, |updater, model, cx| {
updater.dismiss_error(model, cx);
});
}
cx.notify();
model.notify(cx);
}
fn pending_language_server_work<'a>(
@@ -183,7 +203,7 @@ impl ActivityIndicator {
self.project.read(cx).shell_environment_errors(cx)
}
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
fn content_to_render(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Option<Content> {
// Show if any direnv calls failed
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
return Some(Content {
@@ -194,7 +214,7 @@ impl ActivityIndicator {
),
message: error.0.clone(),
on_click: Some(Arc::new(move |this, cx| {
this.project.update(cx, |project, cx| {
this.project.update(cx, |project, model, cx| {
project.remove_environment_error(cx, worktree_id);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
@@ -352,7 +372,7 @@ impl ActivityIndicator {
),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|indicator, cx| {
indicator.project.update(cx, |project, cx| {
indicator.project.update(cx, |project, model, cx| {
project.reset_last_formatting_failure(cx);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
@@ -442,8 +462,12 @@ impl ActivityIndicator {
None
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
self.context_menu_handle.toggle(cx);
fn toggle_language_server_work_context_menu(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.context_menu_handle.toggle(model, cx);
}
}
@@ -452,15 +476,20 @@ impl EventEmitter<Event> for ActivityIndicator {}
const MAX_MESSAGE_LEN: usize = 50;
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_error_message));
let Some(content) = self.content_to_render(cx) else {
.on_action(model.listener(Self::show_error_message))
.on_action(model.listener(Self::dismiss_error_message));
let Some(content) = self.content_to_render(model, cx) else {
return result;
};
let this = cx.view().downgrade();
let this = model.downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
@@ -480,13 +509,15 @@ impl Render for ActivityIndicator {
))
.size(LabelSize::Small),
)
.tooltip(move |cx| Tooltip::text(&content.message, cx))
.tooltip(move |window, cx| {
Tooltip::text(&content.message, cx)
})
} else {
button.child(Label::new(content.message).size(LabelSize::Small))
}
})
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, cx| {
this.on_click(model.listener(move |this, _, model, window, cx| {
handler(this, cx);
}))
.cursor(CursorStyle::PointingHand)
@@ -494,10 +525,10 @@ impl Render for ActivityIndicator {
),
)
.anchor(gpui::AnchorCorner::BottomLeft)
.menu(move |cx| {
.menu(move |window, cx| {
let strong_this = this.upgrade()?;
let mut has_work = false;
let menu = ContextMenu::build(cx, |mut menu, cx| {
let menu = ContextMenu::build(window, cx, |mut menu, model, window, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
@@ -522,16 +553,17 @@ impl Render for ActivityIndicator {
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
this.update(cx, |this, model, cx| {
this.project.update(cx, |project, model, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
model,
cx,
);
});
this.context_menu_handle.hide(cx);
cx.notify();
model.notify(cx);
})
.ok();
},
@@ -554,5 +586,11 @@ impl Render for ActivityIndicator {
}
impl StatusItemView for ActivityIndicator {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
fn set_active_pane_item(
&mut self,
_: Option<&dyn ItemHandle>,
_: &Model<Self>,
_: &mut AppContext,
) {
}
}

View File

@@ -321,9 +321,9 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
)
})
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_active_model(&provider_name, &model_id, cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
LanguageModelRegistry::global(cx).update(cx, |registry, model, cx| {
registry.select_active_model(&provider_name, &model_id, model, cx);
registry.select_inline_alternative_models(inline_alternatives, model, cx);
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{Context as _, IconName, WindowContext};
use ui::{Context as _, IconName};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},
@@ -51,7 +51,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -59,6 +59,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -70,7 +71,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
let message_2 = context.update(cx, |context, cx| {
let message_2 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
@@ -83,8 +84,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1"), (1..1, "2")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
@@ -94,7 +95,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
let message_3 = context.update(cx, |context, cx| {
let message_3 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
.unwrap()
@@ -108,7 +109,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
let message_4 = context.update(cx, |context, cx| {
let message_4 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
.unwrap()
@@ -123,8 +124,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "C"), (5..5, "D")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
@@ -137,7 +138,9 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Deleting across message boundaries merges the messages.
buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(1..4, "")], None, model, cx)
});
assert_eq!(
messages(&context, cx),
vec![
@@ -147,7 +150,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Undoing the deletion should also undo the merge.
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_eq!(
messages(&context, cx),
vec![
@@ -159,7 +162,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Redoing the deletion should also redo the merge.
buffer.update(cx, |buffer, cx| buffer.redo(cx));
buffer.update(cx, |buffer, model, cx| buffer.redo(cx));
assert_eq!(
messages(&context, cx),
vec![
@@ -169,7 +172,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
);
// Ensure we can still insert after a merged message.
let message_5 = context.update(cx, |context, cx| {
let message_5 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
.unwrap()
@@ -193,7 +196,7 @@ fn test_message_splitting(cx: &mut AppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -201,6 +204,7 @@ fn test_message_splitting(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -212,11 +216,11 @@ fn test_message_splitting(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, model, cx)
});
let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
let (_, message_2) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
let message_2 = message_2.unwrap();
// We recycle newlines in the middle of a split message
@@ -229,7 +233,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
let (_, message_3) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
let message_3 = message_3.unwrap();
// We don't recycle newlines at the end of a split message
@@ -243,7 +247,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
let (_, message_4) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
let message_4 = message_4.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
assert_eq!(
@@ -256,7 +260,7 @@ fn test_message_splitting(cx: &mut AppContext) {
]
);
let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
let (_, message_5) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
let message_5 = message_5.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
assert_eq!(
@@ -271,7 +275,7 @@ fn test_message_splitting(cx: &mut AppContext) {
);
let (message_6, message_7) =
context.update(cx, |context, cx| context.split_message(14..16, cx));
context.update(cx, |context, model, cx| context.split_message(14..16, cx));
let message_6 = message_6.unwrap();
let message_7 = message_7.unwrap();
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
@@ -297,7 +301,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -305,6 +309,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -316,20 +321,26 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
vec![(message_1.id, Role::User, 0..0)]
);
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa")], None, model, cx)
});
let message_2 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "bbb")], None, model, cx)
});
let message_3 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(8..8, "ccc")], None, model, cx)
});
assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
assert_eq!(
@@ -351,7 +362,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
);
let message_4 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
})
.unwrap();
@@ -412,7 +423,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -420,6 +431,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -432,7 +444,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
context.update(cx, |_, cx| {
context.update(cx, |_, model, cx| {
cx.subscribe(&context, {
let context_ranges = context_ranges.clone();
move |context, _, event, _| {
@@ -446,7 +458,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
for range in removed {
context_ranges.parsed_commands.remove(range);
context_ranges.parsed_commands.remove(&range);
}
for command in updated {
context_ranges
@@ -467,7 +479,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
// Insert a slash command
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
});
assert_text_and_context_ranges(
@@ -480,7 +492,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Edit the argument of the slash command.
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let edit_offset = buffer.text().find("lib.rs").unwrap();
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
});
@@ -494,7 +506,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Edit the name of the slash command, using one that doesn't exist.
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let edit_offset = buffer.text().find("/file").unwrap();
buffer.edit(
[(edit_offset..edit_offset + "/file".len(), "/unknown")],
@@ -512,7 +524,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
// Undoing the insertion of an non-existent slash command resorts the previous one.
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_text_and_context_ranges(
&buffer,
&context_ranges,
@@ -523,7 +535,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
);
let (command_output_tx, command_output_rx) = mpsc::unbounded();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
context.insert_command_output(
command_source_range,
@@ -619,7 +631,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
cx: &mut TestAppContext,
) {
let mut actual_marked_text = String::new();
buffer.update(cx, |buffer, _| {
buffer.update(cx, |buffer, model, _| {
struct Endpoint {
offset: usize,
marker: char,
@@ -703,7 +715,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
// Create a new context
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
Some(project),
@@ -711,12 +723,13 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
// Insert an assistant message to simulate a response.
let assistant_message_id = context.update(cx, |context, cx| {
let assistant_message_id = context.update(cx, |context, model, cx| {
let user_message_id = context.messages(cx).next().unwrap().id;
context
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
@@ -903,7 +916,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
// When setting the message role to User, the steps are cleared.
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
@@ -932,7 +945,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
// When setting the message role back to Assistant, the steps are reparsed.
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
@@ -968,7 +981,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
let deserialized_context = cx.new_model(|model, cx| {
Context::deserialize(
serialized_context,
Default::default(),
@@ -978,6 +991,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1013,9 +1027,14 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
);
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
context.update(cx, |context, model, cx| {
context.buffer.update(cx, |buffer, model, cx| {
buffer.edit_via_marked_text(
&new_text_marked_with_edits.unindent(),
None,
model,
cx,
);
});
});
cx.executor().run_until_parked();
@@ -1031,7 +1050,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
let (buffer_text, ranges, patches) = context.update(cx, |context, model, cx| {
context.buffer.read_with(cx, |buffer, _| {
let ranges = context
.patches
@@ -1084,7 +1103,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry.clone(),
None,
@@ -1092,31 +1111,32 @@ async fn test_serialization(cx: &mut TestAppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
let message_1 = context.update(cx, |context, cx| {
let message_1 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
});
let message_2 = context.update(cx, |context, cx| {
let message_2 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
.unwrap()
});
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
buffer.finalize_last_transaction();
});
let _message_3 = context.update(cx, |context, cx| {
let _message_3 = context.update(cx, |context, model, cx| {
context
.insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
.unwrap()
});
buffer.update(cx, |buffer, cx| buffer.undo(cx));
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
assert_eq!(
cx.read(|cx| messages(&context, cx)),
@@ -1128,7 +1148,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
);
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new_model(|cx| {
let deserialized_context = cx.new_model(|model, cx| {
Context::deserialize(
serialized_context,
Default::default(),
@@ -1138,6 +1158,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1187,7 +1208,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
i as ReplicaId,
@@ -1198,6 +1219,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
Arc::new(ToolWorkingSet::default()),
None,
None,
model,
cx,
)
});
@@ -1232,15 +1254,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
match rng.gen_range(0..100) {
0..=29 if mutation_count > 0 => {
log::info!("Context {}: edit buffer", context_index);
context.update(cx, |context, cx| {
context
.buffer
.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
context.update(cx, |context, model, cx| {
context.buffer.update(cx, |buffer, model, cx| {
buffer.randomly_edit(&mut rng, 1, cx)
});
});
mutation_count -= 1;
}
30..=44 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let range = context.buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("Context {}: split message at {:?}", context_index, range);
context.split_message(range, cx);
@@ -1248,7 +1270,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
45..=59 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let role = *[Role::User, Role::Assistant, Role::System]
.choose(&mut rng)
@@ -1265,7 +1287,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
60..=74 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
let command_text = "/".to_string()
+ slash_commands
.command_names()
@@ -1274,7 +1296,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.clone()
.as_ref();
let command_range = context.buffer.update(cx, |buffer, cx| {
let command_range = context.buffer.update(cx, |buffer, model, cx| {
let offset = buffer.random_byte_range(0, &mut rng).start;
buffer.edit(
[(offset..offset, format!("\n{}\n", command_text))],
@@ -1342,7 +1364,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
mutation_count -= 1;
}
75..=84 if mutation_count > 0 => {
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let new_status = match rng.gen_range(0..3) {
0 => MessageStatus::Done,
@@ -1390,7 +1412,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
);
network.lock().broadcast(replica_id, ops_to_send);
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
context.update(cx, |context, model, cx| {
context.apply_ops(ops_to_receive, cx)
});
} else if rng.gen_bool(0.1) && replica_id != 0 {
log::info!("Context {}: disconnecting", context_index);
network.lock().disconnect_peer(replica_id);
@@ -1402,7 +1426,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
.map(ContextOperation::from_proto)
.collect::<Result<Vec<_>>>()
.unwrap();
context.update(cx, |context, cx| context.apply_ops(ops, cx));
context.update(cx, |context, model, cx| context.apply_ops(ops, cx));
}
}
}
@@ -1449,7 +1473,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::local(
registry,
None,
@@ -1457,6 +1481,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
model,
cx,
)
});
@@ -1471,7 +1496,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
let message_1 = context.read(cx).message_anchors[0].clone();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
@@ -1484,22 +1509,28 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
"Empty messages should not have any cache anchors."
);
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "aaa")], None, model, cx)
});
let message_2 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(4..4, "bbbbbbb")], None, model, cx)
});
let message_3 = context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(12..12, "cccccc")], None, model, cx)
});
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
@@ -1511,12 +1542,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
0,
"Messages should not be marked for cache before going over the token minimum."
);
context.update(cx, |context, _| {
context.update(cx, |context, model, _| {
context.token_count = Some(20);
});
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, true, cx)
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, true, model, cx)
});
assert_eq!(
messages_cache(&context, cx)
@@ -1528,12 +1559,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
);
context
.update(cx, |context, cx| {
.update(cx, |context, model, cx| {
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
})
.unwrap();
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1544,7 +1575,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
vec![false, true, true, false],
"Most recent message should also be cached if not a speculative request."
);
context.update(cx, |context, cx| {
context.update(cx, |context, model, cx| {
context.update_cache_status_for_completion(cx)
});
assert_eq!(
@@ -1563,8 +1594,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
"All user messages prior to anchor should be marked as cached."
);
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
context.update(cx, |context, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(14..14, "d")], None, model, cx)
});
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1582,8 +1615,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
],
"Modifying a message should invalidate it's cache but leave previous messages."
);
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
context.update(cx, |context, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(2..2, "e")], None, model, cx)
});
context.update(cx, |context, model, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
@@ -1642,8 +1677,9 @@ impl SlashCommand for FakeSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![]))
}
@@ -1657,9 +1693,10 @@ impl SlashCommand for FakeSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
Task::ready(Ok(SlashCommandOutput {
text: format!("Executed fake command: {}", self.0),

View File

@@ -14,9 +14,7 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, Task, WeakModel};
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
@@ -109,11 +107,16 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
let context_server_manager = cx.new_model(|model, cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
model,
cx,
)
});
let mut this = Self {
contexts: Vec::new(),
@@ -127,10 +130,10 @@ impl ContextStore {
slash_commands,
tools,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
_watch_updates: model.spawn(cx, |this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
.await
.log_err();
}
@@ -148,12 +151,12 @@ impl ContextStore {
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.handle_project_changed(project.clone(), model, cx);
this.synchronize_contexts(model, cx);
this.register_context_server_handlers(model, cx);
this
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
.await
.log_err();
@@ -166,7 +169,7 @@ impl ContextStore {
envelope: TypedEnvelope<proto::AdvertiseContexts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.host_contexts = envelope
.payload
.contexts
@@ -176,7 +179,7 @@ impl ContextStore {
summary: context.summary,
})
.collect();
cx.notify();
model.notify(cx);
})
}
@@ -186,7 +189,7 @@ impl ContextStore {
mut cx: AsyncAppContext,
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
let operations = this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host contexts can be opened"));
}
@@ -215,14 +218,14 @@ impl ContextStore {
_: TypedEnvelope<proto::CreateContext>,
mut cx: AsyncAppContext,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
let (context_id, operations) = this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("can only create contexts as the host"));
}
let context = this.create(cx);
let context = this.create(model, cx);
let context_id = context.read(cx).id().clone();
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
model.emit(ContextStoreEvent::ContextCreated(context_id.clone()), cx);
anyhow::Ok((
context_id,
@@ -243,12 +246,14 @@ impl ContextStore {
envelope: TypedEnvelope<proto::UpdateContext>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let context_id = ContextId::from_proto(envelope.payload.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
let operation_proto = envelope.payload.operation.context("invalid operation")?;
let operation = ContextOperation::from_proto(operation_proto)?;
context.update(cx, |context, cx| context.apply_ops([operation], cx));
context.update(cx, |context, model, cx| {
context.apply_ops([operation], model, cx)
});
}
Ok(())
})?
@@ -259,7 +264,7 @@ impl ContextStore {
envelope: TypedEnvelope<proto::SynchronizeContexts>,
mut cx: AsyncAppContext,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host can synchronize contexts"));
}
@@ -298,7 +303,12 @@ impl ContextStore {
})?
}
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
fn handle_project_changed(
&mut self,
_: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
@@ -319,7 +329,7 @@ impl ContextStore {
.client
.subscribe_to_entity(remote_id)
.log_err()
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
.map(|subscription| subscription.set_model(model, &mut cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
@@ -330,22 +340,23 @@ impl ContextStore {
&mut self,
_: Model<Project>,
event: &project::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
project::Event::Reshared => {
self.advertise_contexts(cx);
}
project::Event::HostReshared | project::Event::Rejoined => {
self.synchronize_contexts(cx);
self.synchronize_contexts(model, cx);
}
project::Event::DisconnectedFromHost => {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
*context = ContextHandle::Weak(context.downgrade());
strong_context.update(cx, |context, cx| {
strong_context.update(cx, |context, model, cx| {
if context.replica_id() != ReplicaId::default() {
context.set_capability(language::Capability::ReadOnly, cx);
context.set_capability(language::Capability::ReadOnly, model, cx);
}
});
true
@@ -354,14 +365,14 @@ impl ContextStore {
}
});
self.host_contexts.clear();
cx.notify();
model.notify(cx);
}
_ => {}
}
}
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
pub fn create(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Context> {
let context = cx.new_model(|model, cx| {
Context::local(
self.languages.clone(),
Some(self.project.clone()),
@@ -369,16 +380,18 @@ impl ContextStore {
self.prompt_builder.clone(),
self.slash_commands.clone(),
self.tools.clone(),
model,
cx,
)
});
self.register_context(&context, cx);
self.register_context(&context, model, cx);
context
}
pub fn create_remote_context(
&mut self,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
@@ -394,11 +407,11 @@ impl ContextStore {
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
let context_id = ContextId::from_proto(response.context_id);
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
replica_id,
@@ -409,6 +422,7 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
@@ -423,12 +437,12 @@ impl ContextStore {
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
this.register_context(&context, model, cx);
this.synchronize_contexts(model, cx);
context
}
})
@@ -438,7 +452,8 @@ impl ContextStore {
pub fn open_local_context(
&mut self,
path: PathBuf,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Model<Context>>> {
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
return Task::ready(Ok(existing_context));
@@ -459,9 +474,9 @@ impl ContextStore {
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::deserialize(
saved_context,
path.clone(),
@@ -471,14 +486,15 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.register_context(&context, model, cx);
context
}
})
@@ -514,7 +530,8 @@ impl ContextStore {
pub fn open_remote_context(
&mut self,
context_id: ContextId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
@@ -537,10 +554,10 @@ impl ContextStore {
let prompt_builder = self.prompt_builder.clone();
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
let context = cx.new_model(|model, cx| {
Context::new(
context_id.clone(),
replica_id,
@@ -551,6 +568,7 @@ impl ContextStore {
tools,
Some(project),
Some(telemetry),
model,
cx,
)
})?;
@@ -565,19 +583,24 @@ impl ContextStore {
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
this.register_context(&context, model, cx);
this.synchronize_contexts(model, cx);
context
}
})
})
}
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
fn register_context(
&mut self,
context: &Model<Context>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let handle = if self.project_is_shared {
ContextHandle::Strong(context.clone())
} else {
@@ -592,7 +615,8 @@ impl ContextStore {
&mut self,
context: Model<Context>,
event: &ContextEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
@@ -651,7 +675,7 @@ impl ContextStore {
.ok();
}
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
fn synchronize_contexts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
@@ -674,36 +698,37 @@ impl ContextStore {
project_id,
contexts,
});
cx.spawn(|this, cx| async move {
let response = request.await?;
model
.spawn(cx, |this, cx| async move {
let response = request.await?;
let mut context_ids = Vec::new();
let mut operations = Vec::new();
this.read_with(&cx, |this, cx| {
for context_version_proto in response.contexts {
let context_version = ContextVersion::from_proto(&context_version_proto);
let context_id = ContextId::from_proto(context_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
context_ids.push(context_id);
operations.push(context.read(cx).serialize_ops(&context_version, cx));
let mut context_ids = Vec::new();
let mut operations = Vec::new();
this.read_with(&cx, |this, cx| {
for context_version_proto in response.contexts {
let context_version = ContextVersion::from_proto(&context_version_proto);
let context_id = ContextId::from_proto(context_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
context_ids.push(context_id);
operations.push(context.read(cx).serialize_ops(&context_version, cx));
}
}
})?;
let operations = futures::future::join_all(operations).await;
for (context_id, operations) in context_ids.into_iter().zip(operations) {
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
}
})?;
let operations = futures::future::join_all(operations).await;
for (context_id, operations) in context_ids.into_iter().zip(operations) {
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
@@ -740,9 +765,9 @@ impl ContextStore {
&self.host_contexts
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn reload(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?;
@@ -778,14 +803,14 @@ impl ContextStore {
}
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.contexts_metadata = contexts;
cx.notify();
model.notify(cx);
})
})
}
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
pub fn restart_context_servers(&mut self, model: &Model<Self>, cx: &mut AppContext) {
cx.update_model(
&self.context_server_manager,
|context_server_manager, cx| {
@@ -798,7 +823,7 @@ impl ContextStore {
);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -810,7 +835,8 @@ impl ContextStore {
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let slash_command_working_set = self.slash_commands.clone();
let tool_working_set = self.tools.clone();

File diff suppressed because it is too large Load Diff

View File

@@ -140,7 +140,7 @@ impl ResolvedPatch {
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
}
}
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit(
edits,
Some(AutoindentMode::Block {
@@ -461,7 +461,7 @@ impl AssistantPatch {
// Expand the context ranges of each edit and group edits with overlapping context ranges.
let mut edit_groups_by_buffer = HashMap::default();
for (buffer, edits) in edits_by_buffer {
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
if let Ok(snapshot) = buffer.update(cx, |buffer, model, _| buffer.text_snapshot()) {
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
}
}
@@ -918,7 +918,7 @@ mod tests {
cx: &mut AppContext,
) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
let buffer = cx.new_model(|model, cx| Buffer::local(text.clone(), model, cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
@@ -932,8 +932,9 @@ mod tests {
new_text: String,
cx: &mut AppContext,
) {
let buffer =
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(old_text, model, cx).with_language(Arc::new(rust_lang()), model, cx)
});
let snapshot = buffer.read(cx).snapshot();
let resolved_edits = edits
.into_iter()

View File

@@ -38,8 +38,8 @@ use std::{
use text::LineEnding;
use theme::ThemeSettings;
use ui::{
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, Tooltip, ViewContext, VisualContext,
div, prelude::*, AppContext, IconButtonShape, KeyBinding, ListItem, ListItemSpacing,
ParentElement, Render, SharedString, Styled, Tooltip, VisualContext,
};
use util::{ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -88,7 +88,7 @@ pub fn open_prompt_library(
.find_map(|window| window.downcast::<PromptLibrary>());
if let Some(existing_window) = existing_window {
existing_window
.update(cx, |_, cx| cx.activate_window())
.update(cx, |_, model, cx| cx.activate_window())
.ok();
Task::ready(Ok(existing_window))
} else {
@@ -109,7 +109,11 @@ pub fn open_prompt_library(
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|cx| {
cx.new_model(|model, cx| {
PromptLibrary::new(store, language_registry, model, cx)
})
},
)
})?
})
@@ -121,14 +125,14 @@ pub struct PromptLibrary {
language_registry: Arc<LanguageRegistry>,
prompt_editors: HashMap<PromptId, PromptEditor>,
active_prompt_id: Option<PromptId>,
picker: View<Picker<PromptPickerDelegate>>,
picker: Model<Picker<PromptPickerDelegate>>,
pending_load: Task<()>,
_subscriptions: Vec<Subscription>,
}
struct PromptEditor {
title_editor: View<Editor>,
body_editor: View<Editor>,
title_editor: Model<Editor>,
body_editor: Model<Editor>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
next_title_and_body_to_save: Option<(String, Rope)>,
@@ -158,7 +162,11 @@ impl PickerDelegate for PromptPickerDelegate {
self.matches.len()
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
fn no_matches_text(
&self,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> SharedString {
if self.store.prompt_count() == 0 {
"No prompts.".into()
} else {
@@ -170,23 +178,31 @@ impl PickerDelegate for PromptPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_index = ix;
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Selected {
prompt_id: prompt.id,
});
model.emit(
cx,
PromptPickerEvent::Selected {
prompt_id: prompt.id,
},
);
}
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Search...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let search = self.store.search(query);
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let (matches, selected_index) = cx
.background_executor()
.spawn(async move {
@@ -201,30 +217,34 @@ impl PickerDelegate for PromptPickerDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.delegate.matches = matches;
this.delegate.set_selected_index(selected_index, cx);
cx.notify();
model.notify(cx);
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
});
model.emit(
cx,
PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
},
);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let prompt = self.matches.get(ix)?;
let default = prompt.default;
@@ -241,9 +261,9 @@ impl PickerDelegate for PromptPickerDelegate {
.selected(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
.tooltip(move |window, cx| Tooltip::text("Remove from Default Prompt", cx))
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
}))
}))
.end_hover_slot(
@@ -253,11 +273,12 @@ impl PickerDelegate for PromptPickerDelegate {
div()
.id("built-in-prompt")
.child(Icon::new(IconName::FileLock).color(Color::Muted))
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
window,
cx,
)
})
@@ -266,9 +287,9 @@ impl PickerDelegate for PromptPickerDelegate {
IconButton::new("delete-prompt", IconName::Trash)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
.tooltip(move |window, cx| Tooltip::text("Delete Prompt", cx))
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::Deleted { prompt_id }, cx)
}))
.into_any_element()
})
@@ -278,7 +299,7 @@ impl PickerDelegate for PromptPickerDelegate {
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
@@ -288,15 +309,20 @@ impl PickerDelegate for PromptPickerDelegate {
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
.on_click(model.listener(move |_, _, model, window, cx| {
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
})),
),
);
Some(element)
}
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
fn render_editor(
&self,
editor: &Model<Editor>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Div {
h_flex()
.bg(cx.theme().colors().editor_background)
.rounded_md()
@@ -313,7 +339,8 @@ impl PromptLibrary {
fn new(
store: Arc<PromptStore>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let delegate = PromptPickerDelegate {
store: store.clone(),
@@ -321,11 +348,11 @@ impl PromptLibrary {
matches: Vec::new(),
};
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
let picker = cx.new_model(|model, cx| {
let picker = Picker::uniform_list(delegate, model, cx)
.modal(false)
.max_height(None);
picker.focus(cx);
picker.focus(window, cx);
picker
});
Self {
@@ -341,47 +368,52 @@ impl PromptLibrary {
fn handle_picker_event(
&mut self,
_: View<Picker<PromptPickerDelegate>>,
_: Model<Picker<PromptPickerDelegate>>,
event: &PromptPickerEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
PromptPickerEvent::Selected { prompt_id } => {
self.load_prompt(*prompt_id, false, cx);
self.load_prompt(*prompt_id, false, model, cx);
}
PromptPickerEvent::Confirmed { prompt_id } => {
self.load_prompt(*prompt_id, true, cx);
self.load_prompt(*prompt_id, true, model, cx);
}
PromptPickerEvent::ToggledDefault { prompt_id } => {
self.toggle_default_for_prompt(*prompt_id, cx);
self.toggle_default_for_prompt(*prompt_id, model, cx);
}
PromptPickerEvent::Deleted { prompt_id } => {
self.delete_prompt(*prompt_id, cx);
self.delete_prompt(*prompt_id, model, cx);
}
}
}
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn new_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
// If we already have an untitled prompt, use that instead
// of creating a new one.
if let Some(metadata) = self.store.first() {
if metadata.title.is_none() {
self.load_prompt(metadata.id, true, cx);
self.load_prompt(metadata.id, true, model, cx);
return;
}
}
let prompt_id = PromptId::new();
let save = self.store.save(prompt_id, None, false, "".into());
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.spawn(|this, mut cx| async move {
save.await?;
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
})
.detach_and_log_err(cx);
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model
.spawn(cx, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |this, model, cx| {
this.load_prompt(prompt_id, true, cx)
})
})
.detach_and_log_err(cx);
}
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn save_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
if prompt_id.is_built_in() {
@@ -391,7 +423,7 @@ impl PromptLibrary {
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
let title = prompt_editor.title_editor.read(cx).text(cx);
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
let body = prompt_editor.body_editor.update(cx, |editor, model, cx| {
editor
.buffer()
.read(cx)
@@ -407,10 +439,10 @@ impl PromptLibrary {
prompt_editor.next_title_and_body_to_save = Some((title, body));
if prompt_editor.pending_save.is_none() {
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
prompt_editor.pending_save = Some(model.spawn(cx, |this, mut cx| {
async move {
loop {
let title_and_body = this.update(&mut cx, |this, _| {
let title_and_body = this.update(&mut cx, |this, _, _| {
this.prompt_editors
.get_mut(&prompt_id)?
.next_title_and_body_to_save
@@ -427,9 +459,10 @@ impl PromptLibrary {
.save(prompt_id, title, prompt_metadata.default, body)
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
this.update(&mut cx, |this, model, cx| {
this.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
})?;
executor.timer(SAVE_THROTTLE).await;
@@ -449,77 +482,90 @@ impl PromptLibrary {
}
}
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn delete_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.delete_prompt(active_prompt_id, cx);
self.delete_prompt(active_prompt_id, model, cx);
}
}
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn duplicate_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.duplicate_prompt(active_prompt_id, cx);
self.duplicate_prompt(active_prompt_id, model, cx);
}
}
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
pub fn toggle_default_for_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.toggle_default_for_prompt(active_prompt_id, cx);
self.toggle_default_for_prompt(active_prompt_id, model, cx);
}
}
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn toggle_default_for_prompt(
&mut self,
prompt_id: PromptId,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
self.store
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
.detach_and_log_err(cx);
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
}
}
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
pub fn load_prompt(
&mut self,
prompt_id: PromptId,
focus: bool,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
if focus {
prompt_editor
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
.update(cx, |editor, model, cx| editor.focus(window, cx));
}
self.set_active_prompt(Some(prompt_id), cx);
self.set_active_prompt(Some(prompt_id), model, cx);
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
let language_registry = self.language_registry.clone();
let prompt = self.store.load(prompt_id);
self.pending_load = cx.spawn(|this, mut cx| async move {
self.pending_load = model.spawn(cx, |this, mut cx| async move {
let prompt = prompt.await;
let markdown = language_registry.language_for_name("Markdown").await;
this.update(&mut cx, |this, cx| match prompt {
this.update(&mut cx, |this, model, cx| match prompt {
Ok(prompt) => {
let title_editor = cx.new_view(|cx| {
let mut editor = Editor::auto_width(cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
let title_editor = cx.new_model(|model, cx| {
let mut editor = Editor::auto_width(model, cx);
editor.set_placeholder_text("Untitled", model, cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), model, cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.set_show_inline_completions(Some(false), model, cx);
}
editor
});
let body_editor = cx.new_view(|cx| {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
let body_editor = cx.new_model(|model, cx| {
// todo!: Remove this type annotation on cx
let buffer = cx.new_model(|model, cx: &mut AppContext| {
let mut buffer = Buffer::local(prompt, model, cx);
buffer.set_language(markdown.log_err(), model, cx);
buffer.set_language_registry(language_registry);
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
let mut editor = Editor::for_buffer(buffer, None, model, cx);
if prompt_id.is_built_in() {
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
editor.set_show_inline_completions(Some(false), model, cx);
}
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, model, cx);
editor.set_show_gutter(false, model, cx);
editor.set_show_wrap_guides(false, model, cx);
editor.set_show_indent_guides(false, model, cx);
editor.set_use_modal_editing(false);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_completion_provider(Some(Box::new(
@@ -530,7 +576,7 @@ impl PromptLibrary {
),
)));
if focus {
editor.focus(cx);
editor.focus(window, cx);
}
editor
});
@@ -567,9 +613,14 @@ impl PromptLibrary {
}
}
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
fn set_active_prompt(
&mut self,
prompt_id: Option<PromptId>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.active_prompt_id = prompt_id;
self.picker.update(cx, |picker, cx| {
self.picker.update(cx, |picker, model, cx| {
if let Some(prompt_id) = prompt_id {
if picker
.delegate
@@ -589,13 +640,13 @@ impl PromptLibrary {
}
}
} else {
picker.focus(cx);
picker.focus(window);
}
});
cx.notify();
model.notify(cx);
}
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn delete_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
if let Some(metadata) = self.store.metadata(prompt_id) {
let confirmation = cx.prompt(
PromptLevel::Warning,
@@ -607,25 +658,32 @@ impl PromptLibrary {
&["Delete", "Cancel"],
);
cx.spawn(|this, mut cx| async move {
if confirmation.await.ok() == Some(0) {
this.update(&mut cx, |this, cx| {
if this.active_prompt_id == Some(prompt_id) {
this.set_active_prompt(None, cx);
}
this.prompt_editors.remove(&prompt_id);
this.store.delete(prompt_id).detach_and_log_err(cx);
this.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
model
.spawn(cx, |this, mut cx| async move {
if confirmation.await.ok() == Some(0) {
this.update(&mut cx, |this, model, cx| {
if this.active_prompt_id == Some(prompt_id) {
this.set_active_prompt(None, cx);
}
this.prompt_editors.remove(&prompt_id);
this.store.delete(prompt_id).detach_and_log_err(cx);
this.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model.notify(cx);
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
pub fn duplicate_prompt(
&mut self,
prompt_id: PromptId,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
const DUPLICATE_SUFFIX: &str = " copy";
let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
@@ -655,31 +713,39 @@ impl PromptLibrary {
let save = self
.store
.save(new_id, Some(title.into()), false, body.into());
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.spawn(|this, mut cx| async move {
save.await?;
this.update(&mut cx, |prompt_library, cx| {
prompt_library.load_prompt(new_id, true, cx)
self.picker
.update(cx, |picker, model, cx| picker.refresh(cx));
model
.spawn(cx, |this, mut cx| async move {
save.await?;
this.update(&mut cx, |prompt_library, cx| {
prompt_library.load_prompt(new_id, true, cx)
})
})
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
}
}
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
fn focus_active_prompt(&mut self, _: &Tab, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_prompt) = self.active_prompt_id {
self.prompt_editors[&active_prompt]
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
.update(cx, |editor, model, cx| editor.focus(window, cx));
cx.stop_propagation();
}
}
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| picker.focus(cx));
fn focus_picker(&mut self, _: &menu::Cancel, model: &Model<Self>, cx: &mut AppContext) {
self.picker
.update(cx, |picker, model, cx| picker.focus(window));
}
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
pub fn inline_assist(
&mut self,
action: &InlineAssist,
model: &Model<Self>,
cx: &mut AppContext,
) {
let Some(active_prompt_id) = self.active_prompt_id else {
cx.propagate();
return;
@@ -693,13 +759,13 @@ impl PromptLibrary {
let initial_prompt = action.prompt.clone();
if provider.is_authenticated(cx) {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
})
} else {
for window in cx.windows() {
if let Some(workspace) = window.downcast::<Workspace>() {
let panel = workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
cx.activate_window();
workspace.focus_panel::<AssistantPanel>(cx)
})
@@ -713,7 +779,12 @@ impl PromptLibrary {
}
}
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
fn move_down_from_title(
&mut self,
_: &editor::actions::MoveDown,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.body_editor);
@@ -721,7 +792,12 @@ impl PromptLibrary {
}
}
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
fn move_up_from_body(
&mut self,
_: &editor::actions::MoveUp,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.title_editor);
@@ -732,18 +808,19 @@ impl PromptLibrary {
fn handle_prompt_title_editor_event(
&mut self,
prompt_id: PromptId,
title_editor: View<Editor>,
title_editor: Model<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, model, cx);
self.count_tokens(prompt_id, model, cx);
}
EditorEvent::Blurred => {
title_editor.update(cx, |title_editor, cx| {
title_editor.change_selections(None, cx, |selections| {
title_editor.update(cx, |title_editor, model, cx| {
title_editor.change_selections(None, model, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -756,18 +833,19 @@ impl PromptLibrary {
fn handle_prompt_body_editor_event(
&mut self,
prompt_id: PromptId,
body_editor: View<Editor>,
body_editor: Model<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
self.save_prompt(prompt_id, model, cx);
self.count_tokens(prompt_id, model, cx);
}
EditorEvent::Blurred => {
body_editor.update(cx, |body_editor, cx| {
body_editor.change_selections(None, cx, |selections| {
body_editor.update(cx, |body_editor, model, cx| {
body_editor.change_selections(None, model, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
@@ -777,7 +855,7 @@ impl PromptLibrary {
}
}
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
fn count_tokens(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -785,7 +863,7 @@ impl PromptLibrary {
let editor = &prompt.body_editor.read(cx);
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
let body = buffer.as_rope().clone();
prompt.pending_token_count = cx.spawn(|this, mut cx| {
prompt.pending_token_count = model.spawn(cx, |this, mut cx| {
async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -808,10 +886,10 @@ impl PromptLibrary {
})?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
prompt_editor.token_count = Some(token_count);
cx.notify();
model.notify(cx);
})
}
.log_err()
@@ -819,7 +897,7 @@ impl PromptLibrary {
}
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_prompt_list(&mut self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
v_flex()
.id("prompt-list")
.capture_action(cx.listener(Self::focus_active_prompt))
@@ -839,7 +917,9 @@ impl PromptLibrary {
IconButton::new("new-prompt", IconName::Plus)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
.tooltip(move |window, cx| {
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(NewPrompt));
}),
@@ -848,7 +928,11 @@ impl PromptLibrary {
.child(div().flex_grow().child(self.picker.clone()))
}
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
fn render_active_prompt(
&mut self,
model: &Model<PromptLibrary>,
cx: &mut AppContext,
) -> gpui::Stateful<Div> {
div()
.w_2_3()
.h_full()
@@ -873,7 +957,7 @@ impl PromptLibrary {
.overflow_hidden()
.pl(DynamicSpacing::Base16.rems(cx))
.pt(DynamicSpacing::Base08.rems(cx))
.on_click(cx.listener(move |_, _, cx| {
.on_click(model.listener(move |_, _, model, window, cx| {
cx.focus(&focus_handle);
}))
.child(
@@ -927,7 +1011,7 @@ impl PromptLibrary {
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
editor::make_inlay_hints_style(model, cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
@@ -959,7 +1043,7 @@ impl PromptLibrary {
h_flex()
.id("token_count")
.tooltip(move |cx| {
.tooltip(move |window, cx| {
let token_count =
token_count.clone();
@@ -978,6 +1062,7 @@ impl PromptLibrary {
.0)
.unwrap_or_default()
),
model,
cx,
)
})
@@ -997,11 +1082,12 @@ impl PromptLibrary {
Icon::new(IconName::FileLock)
.color(Color::Muted),
)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Built-in prompt",
None,
BUILT_IN_TOOLTIP_TEXT,
model,
cx,
)
})
@@ -1015,10 +1101,11 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
model,
cx,
)
})
@@ -1036,10 +1123,11 @@ impl PromptLibrary {
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::for_action(
"Duplicate Prompt",
&DuplicatePrompt,
model,
cx,
)
})
@@ -1064,7 +1152,7 @@ impl PromptLibrary {
})
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
@@ -1098,8 +1186,13 @@ impl PromptLibrary {
}
impl Render for PromptLibrary {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font = theme::setup_ui_font(cx);
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let ui_font = theme::setup_ui_font(window, cx);
let theme = cx.theme().clone();
h_flex()
@@ -1115,7 +1208,7 @@ impl Render for PromptLibrary {
.overflow_hidden()
.font(ui_font)
.text_color(theme.colors().text)
.child(self.render_prompt_list(cx))
.child(self.render_prompt_list(model, cx))
.map(|el| {
if self.store.prompt_count() == 0 {
el.child(
@@ -1151,7 +1244,7 @@ impl Render for PromptLibrary {
Button::new("create-prompt", "New Prompt")
.full_width()
.key_binding(KeyBinding::for_action(
&NewPrompt, cx,
&NewPrompt, window, cx,
))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone())
@@ -1162,7 +1255,7 @@ impl Render for PromptLibrary {
),
)
} else {
el.child(self.render_active_prompt(cx))
el.child(self.render_active_prompt(model, cx))
}
})
}

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use gpui::{AppContext, AppContext, Model, Task, WeakView};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::CompletionIntent;
@@ -41,8 +41,8 @@ pub mod terminal_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
editor: Option<WeakModel<ContextEditor>>,
workspace: Option<WeakModel<Workspace>>,
}
pub(crate) struct SlashCommandLine {
@@ -55,8 +55,8 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
editor: Option<WeakModel<ContextEditor>>,
workspace: Option<WeakModel<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -71,7 +71,8 @@ impl SlashCommandCompletionProvider {
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
@@ -120,12 +121,12 @@ impl SlashCommandCompletionProvider {
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent, cx: &mut WindowContext| {
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| {
editor.run_command(
command_range.clone(),
&command_name,
@@ -165,7 +166,8 @@ impl SlashCommandCompletionProvider {
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
last_argument_range: Range<Anchor>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
@@ -203,12 +205,12 @@ impl SlashCommandCompletionProvider {
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent, cx: &mut WindowContext| {
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
if new_argument.after_completion.run()
|| intent.is_complete()
{
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| {
editor.run_command(
command_range.clone(),
&command_name,
@@ -260,10 +262,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer: &Model<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
buffer.update(cx, |buffer, model, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
@@ -318,7 +321,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
cx,
)
} else {
self.complete_command_name(&name, command_range, last_argument_range, cx)
self.complete_command_name(&name, command_range, last_argument_range, model, cx)
}
}
@@ -327,7 +330,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
_: &Model<Editor>,
_: &mut AppContext,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
@@ -337,7 +341,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
_: &Model<Editor>,
_: &mut AppContext,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
@@ -348,7 +353,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);

View File

@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use ui::{prelude::*, BorrowAppContext};
use util::ResultExt;
use workspace::Workspace;
@@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
// There's no autocomplete for a prompt, since it's arbitrary text.
// However, we can use this opportunity to kick off a drain of the backlog.
@@ -76,7 +77,7 @@ impl SlashCommand for AutoCommand {
let cx: &mut AppContext = cx;
cx.spawn(|cx: gpui::AsyncAppContext| async move {
cx.spawn(|cx: AsyncAppContext| async move {
let task = project_index.read_with(&cx, |project_index, cx| {
project_index.flush_summary_backlogs(cx)
})?;
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
@@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand {
return Task::ready(Err(anyhow!("no project indexer")));
};
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
let task = cx.spawn(|cx: AsyncAppContext| async move {
let summaries = project_index
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
.await?;

View File

@@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -122,11 +123,12 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let output = workspace.update(cx, |workspace, model, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);

View File

@@ -8,7 +8,7 @@ use context_server::{
manager::{ContextServer, ContextServerManager},
types::Prompt,
};
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
use gpui::{AppContext, Model, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
return Task::ready(Err(anyhow!("Failed to complete argument")));
@@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();

View File

@@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -47,9 +48,9 @@ impl SlashCommand for DefaultSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
};
use collections::HashSet;
use futures::future;
use gpui::{Task, WeakView, WindowContext};
use gpui::{Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt;
@@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand {
_arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let mut paths = HashSet::default();
let mut file_command_old_outputs = Vec::new();
@@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand {
context_buffer.clone(),
workspace.clone(),
delegate.clone(),
model,
cx,
));
}

View File

@@ -30,7 +30,7 @@ impl DiagnosticsSlashCommand {
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
@@ -118,8 +118,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
@@ -172,9 +172,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));

View File

@@ -43,7 +43,7 @@ impl DocsSlashCommand {
/// access the workspace so we can read the project.
fn ensure_rust_doc_providers_are_registered(
&self,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
cx: &mut AppContext,
) {
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
@@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rust_doc_providers_are_registered(workspace, cx);
@@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
if arguments.is_empty() {
return Task::ready(Err(anyhow!("missing an argument")));

View File

@@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -135,9 +136,9 @@ impl SlashCommand for FetchSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(argument) = arguments.first() else {
return Task::ready(Err(anyhow!("missing URL")));

View File

@@ -28,7 +28,7 @@ impl FileSlashCommand {
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
@@ -134,8 +134,8 @@ impl SlashCommand for FileSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
@@ -187,9 +187,9 @@ impl SlashCommand for FileSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));

View File

@@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let now = Local::now();
let text = format!("Today is {now}.", now = now.to_rfc2822());

View File

@@ -6,7 +6,7 @@ use crate::PromptBuilder;
use anyhow::{anyhow, Result};
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
use feature_flags::FeatureFlag;
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use language::{Anchor, CodeLabel, LspAdapterDelegate};
use language_model::{LanguageModelRegistry, LanguageModelTool};
use schemars::JsonSchema;
@@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let model_registry = LanguageModelRegistry::read_global(cx);
let current_model = model_registry.active_model();

View File

@@ -37,8 +37,8 @@ impl SlashCommand for PromptSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
@@ -64,9 +64,9 @@ impl SlashCommand for PromptSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let title = arguments.to_owned().join(" ");
if title.trim().is_empty() {

View File

@@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -69,9 +70,9 @@ impl SlashCommand for SearchSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: language::BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window, cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));

View File

@@ -9,7 +9,7 @@ use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ui::{IconName, SharedString, WindowContext};
use ui::{IconName, SharedString};
use workspace::Workspace;
pub(crate) struct SelectionCommand;
@@ -47,8 +47,9 @@ impl SlashCommand for SelectionCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -58,9 +59,10 @@ impl SlashCommand for SelectionCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let mut events = vec![];

View File

@@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let (events_tx, events_rx) = mpsc::unbounded();
cx.background_executor()

View File

@@ -8,7 +8,7 @@ use gpui::{Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
use ui::{IconName, WindowContext};
use ui::IconName;
use workspace::Workspace;
pub(crate) struct OutlineSlashCommand;
@@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
@@ -49,11 +50,12 @@ impl SlashCommand for OutlineSlashCommand {
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let output = workspace.update(cx, |workspace, model, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};

View File

@@ -12,7 +12,7 @@ use std::{
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ActiveTheme, WindowContext};
use ui::{prelude::*, ActiveTheme};
use util::ResultExt;
use workspace::Workspace;
@@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let mut has_all_tabs_completion_item = false;
let argument_set = arguments
@@ -73,8 +74,8 @@ impl SlashCommand for TabSlashCommand {
let active_item_path = workspace.as_ref().and_then(|workspace| {
workspace
.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
.update(cx, |workspace, model, cx| {
let snapshot = active_item_buffer(workspace, model, cx).ok()?;
snapshot.resolve_file_path(cx, true)
})
.ok()
@@ -82,7 +83,7 @@ impl SlashCommand for TabSlashCommand {
});
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
tab_items_for_queries(workspace, &[current_query], cancel, false, model, cx);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
cx.spawn(|_| async move {
@@ -137,9 +138,10 @@ impl SlashCommand for TabSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let tab_items_search = tab_items_for_queries(
Some(workspace),
@@ -160,11 +162,12 @@ impl SlashCommand for TabSlashCommand {
}
fn tab_items_for_queries(
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
queries: &[String],
cancel: Arc<AtomicBool>,
strict_match: bool,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
let queries = queries.to_owned();
@@ -174,7 +177,7 @@ fn tab_items_for_queries(
.context("no workspace")?
.update(&mut cx, |workspace, cx| {
if strict_match && empty_query {
let snapshot = active_item_buffer(workspace, cx)?;
let snapshot = active_item_buffer(workspace, model, cx)?;
let full_path = snapshot.resolve_file_path(cx, true);
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
}
@@ -285,7 +288,8 @@ fn tab_items_for_queries(
fn active_item_buffer(
workspace: &mut Workspace,
cx: &mut ui::ViewContext<Workspace>,
model: &Model<Workspace>,
cx: &mut AppContext,
) -> anyhow::Result<BufferSnapshot> {
let active_editor = workspace
.active_item(cx)

View File

@@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand {
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
@@ -64,15 +65,16 @@ impl SlashCommand for TerminalSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
let Some(active_terminal) = resolve_active_terminal(&workspace, model, cx) else {
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
};
@@ -107,9 +109,10 @@ impl SlashCommand for TerminalSlashCommand {
}
fn resolve_active_terminal(
workspace: &View<Workspace>,
cx: &WindowContext,
) -> Option<View<TerminalView>> {
workspace: &Model<Workspace>,
window: &Window,
cx: &AppContext,
) -> Option<Model<TerminalView>> {
if let Some(terminal_view) = workspace
.read(cx)
.active_item(cx)

View File

@@ -10,7 +10,7 @@ use crate::SlashCommandWorkingSet;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
trigger: T,
}
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
renderer: fn(&mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
on_confirm: fn(&mut gpui::Window, &mut gpui::AppContext),
},
}
@@ -44,14 +44,14 @@ impl AsRef<str> for SlashCommandEntry {
pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandEntry>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
selected_index: usize,
}
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
active_context_editor: WeakModel<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
@@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
cx.notify();
model.notify(cx);
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Select a command...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let all_commands = self.all_commands.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
@@ -104,10 +109,10 @@ impl PickerDelegate for SlashCommandDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.delegate.filtered_commands = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
model.notify(cx);
})
.ok();
})
@@ -139,25 +144,31 @@ impl PickerDelegate for SlashCommandDelegate {
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(
&mut self,
_secondary: bool,
model: &Model<Picker>,
window: &mut Window,
cx: &mut AppContext,
) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
match command {
SlashCommandEntry::Info(info) => {
self.active_context_editor
.update(cx, |context_editor, cx| {
.update(cx, |context_editor, model, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
on_confirm(window, cx);
}
}
cx.emit(DismissEvent);
model.emit(DismissEvent, cx);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
@@ -167,7 +178,8 @@ impl PickerDelegate for SlashCommandDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
@@ -179,7 +191,10 @@ impl PickerDelegate for SlashCommandDelegate {
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
move |cx| {
cx.new_model(|_, _| Tooltip::new(description.clone()))
.into()
}
})
.child(
v_flex()
@@ -230,14 +245,14 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(renderer(cx)),
.child(renderer(window, cx)),
),
}
}
}
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
let all_models = self
.working_set
.featured_command_names(cx)
@@ -305,14 +320,15 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
let picker_view = cx.new_model(|model, cx| {
let picker =
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()));
picker
});
let handle = self
.active_context_editor
.update(cx, |this, _| this.slash_menu_handle.clone())
.update(cx, |this, model, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,13 @@ use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
pub struct ActiveThread {
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
@@ -31,13 +31,14 @@ pub struct ActiveThread {
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.observe(&thread, |_, _, cx| model.notify(cx)),
cx.subscribe(&thread, Self::handle_thread_event),
];
@@ -49,9 +50,9 @@ impl ActiveThread {
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
let this = model.downgrade();
move |ix, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
this.update(cx, |this, model, cx| this.render_message(ix, model, cx))
.unwrap()
}
}),
@@ -60,7 +61,7 @@ impl ActiveThread {
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), cx);
this.push_message(&message.id, message.text.clone(), model, cx);
}
this
@@ -82,7 +83,13 @@ impl ActiveThread {
self.last_error.take();
}
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
fn push_message(
&mut self,
id: &MessageId,
text: String,
model: &Model<Self>,
cx: &mut AppContext,
) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
@@ -91,7 +98,7 @@ impl ActiveThread {
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let mut text_style = cx.text_style();
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
@@ -120,12 +127,14 @@ impl ActiveThread {
..Default::default()
};
let markdown = cx.new_view(|cx| {
let markdown = cx.new_model(|model, cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
model,
window,
cx,
)
});
@@ -136,7 +145,8 @@ impl ActiveThread {
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
ThreadEvent::ShowError(error) => {
@@ -146,8 +156,8 @@ impl ActiveThread {
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
markdown.update(cx, |markdown, model, cx| {
markdown.append(text, model, cx);
});
}
}
@@ -158,10 +168,10 @@ impl ActiveThread {
.message(*message_id)
.map(|message| message.text.clone())
{
self.push_message(message_id, message_text, cx);
self.push_message(message_id, message_text, model, cx);
}
cx.notify();
model.notify(cx);
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
@@ -175,13 +185,14 @@ impl ActiveThread {
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
let task = tool.run(tool_use.input, self.workspace.clone(), model, cx);
self.thread.update(cx, |thread, cx| {
self.thread.update(cx, |thread, model, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
model,
cx,
);
});
@@ -192,7 +203,7 @@ impl ActiveThread {
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_message(&self, ix: usize, model: &Model<Self>, cx: &mut AppContext) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
@@ -236,7 +247,12 @@ impl ActiveThread {
}
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
_window: &mut gpui::Window,
_cx: &mut AppContext,
) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
}
}

View File

@@ -4,9 +4,8 @@ use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
prelude::*, px, svg, Action, AnyElement, AppContext, AppContext, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, Pixels, Task, View, WeakView,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
@@ -25,8 +24,8 @@ use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|workspace: &mut Workspace, model: &Model<Workspace>, _cx: &mut AppContext| {
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
@@ -40,22 +39,23 @@ enum ActiveView {
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
thread: Model<ActiveThread>,
message_editor: Model<MessageEditor>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
history: Model<ThreadHistory>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
workspace: WeakModel<Workspace>,
window: AnyWindowHandle,
cx: AsyncAppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
@@ -66,7 +66,7 @@ impl AssistantPanel {
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
cx.new_model(|model, cx| Self::new(workspace, thread_store, tools, model, cx))
})
})
}
@@ -75,34 +75,37 @@ impl AssistantPanel {
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let thread = thread_store.update(cx, |this, model, cx| this.create_thread(model, cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
let weak_self = model.downgrade();
Self {
active_view: ActiveView::Thread,
workspace: workspace.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
thread: cx.new_model(|model, cx| {
ActiveThread::new(
thread.clone(),
workspace,
language_registry,
tools.clone(),
model,
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
message_editor: cx.new_model(|model, cx| MessageEditor::new(thread.clone(), model, cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
history: cx
.new_model(|model, cx| ThreadHistory::new(weak_self, thread_store, model, cx)),
}
}
@@ -110,50 +113,63 @@ impl AssistantPanel {
self.local_timezone
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
fn new_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let thread = self
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
.update(cx, |this, model, cx| this.create_thread(model, cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
self.thread = cx.new_model(|model, cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
}
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
pub(crate) fn open_thread(
&mut self,
thread_id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) {
let Some(thread) = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx))
.update(cx, |this, model, cx| this.open_thread(thread_id, model, cx))
else {
return;
};
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
self.thread = cx.new_model(|model, cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
model,
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
self.message_editor.focus_handle(cx).focus(window);
}
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
pub(crate) fn delete_thread(
&mut self,
thread_id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.thread_store.update(cx, |this, model, cx| {
this.delete_thread(thread_id, model, cx)
});
}
}
@@ -173,7 +189,7 @@ impl Panel for AssistantPanel {
"AssistantPanel2"
}
fn position(&self, _cx: &WindowContext) -> DockPosition {
fn position(&self, _window: &Window, cx: &AppContext) -> DockPosition {
DockPosition::Right
}
@@ -181,25 +197,26 @@ impl Panel for AssistantPanel {
true
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn set_position(&mut self, _position: DockPosition, model: &Model<Self>, _cx: &mut AppContext) {
}
fn size(&self, _cx: &WindowContext) -> Pixels {
fn size(&self, _window: &Window, cx: &AppContext) -> Pixels {
px(640.)
}
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
fn set_size(&mut self, _size: Option<Pixels>, model: &Model<Self>, _cx: &mut AppContext) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, model: &Model<Self>, _cx: &mut AppContext) {}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
fn icon(&self, _window: &Window, cx: &AppContext) -> Option<IconName> {
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
Some("Assistant Panel")
}
@@ -209,7 +226,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_toolbar(&self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
h_flex()
@@ -225,7 +242,7 @@ impl AssistantPanel {
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(self.render_language_model_selector(cx))
.child(self.render_language_model_selector(model, cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
@@ -239,6 +256,7 @@ impl AssistantPanel {
"New Thread",
&NewThread,
&focus_handle,
window,
cx,
)
}
@@ -259,6 +277,7 @@ impl AssistantPanel {
"Open History",
&OpenHistory,
&focus_handle,
window,
cx,
)
}
@@ -272,7 +291,7 @@ impl AssistantPanel {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.tooltip(move |window, cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
println!("Configure Assistant");
}),
@@ -280,7 +299,11 @@ impl AssistantPanel {
)
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_language_model_selector(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
@@ -327,22 +350,32 @@ impl AssistantPanel {
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
.tooltip(move |window, cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, model, cx)
}),
)
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_active_thread_or_empty_state(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
return self.render_thread_empty_state(model, cx).into_any_element();
}
self.thread.clone().into_any()
}
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_thread_empty_state(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, cx| this.recent_threads(3, cx));
.update(cx, |this, model, cx| this.recent_threads(3, model, cx));
v_flex()
.gap_2()
@@ -410,7 +443,7 @@ impl AssistantPanel {
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
.map(|thread| PastThread::new(thread, model.downgrade())),
),
)
.child(
@@ -421,6 +454,7 @@ impl AssistantPanel {
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
model,
cx,
))
.on_click(move |_event, cx| {
@@ -431,7 +465,7 @@ impl AssistantPanel {
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
fn render_last_error(&self, model: &Model<Self>, cx: &mut AppContext) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
@@ -445,19 +479,23 @@ impl AssistantPanel {
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::PaymentRequired => self.render_payment_required_error(model, cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
self.render_max_monthly_spend_reached_error(model, cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, cx)
self.render_error_message(&error_message, model, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_payment_required_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
v_flex()
@@ -482,28 +520,32 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
model.notify(cx);
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.notify();
model.notify(cx);
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
fn render_max_monthly_spend_reached_error(
&self,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
@@ -528,23 +570,23 @@ impl AssistantPanel {
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.thread.update(cx, |this, _cx| {
model.listener(|this, model, _, cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
model.notify(cx);
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.notify();
model.notify(cx);
},
))),
)
@@ -554,7 +596,8 @@ impl AssistantPanel {
fn render_error_message(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> AnyElement {
v_flex()
.gap_0p5()
@@ -581,11 +624,11 @@ impl AssistantPanel {
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.thread.update(cx, |this, model, _cx| {
this.clear_last_error();
});
cx.notify();
model.notify(cx);
},
))),
)
@@ -594,7 +637,12 @@ impl AssistantPanel {
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
v_flex()
.key_context("AssistantPanel2")
.justify_between()
@@ -604,20 +652,20 @@ impl Render for AssistantPanel {
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
this.history.focus_handle(cx).focus(window);
model.notify(cx);
}))
.child(self.render_toolbar(cx))
.child(self.render_toolbar(model, cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx))
.child(self.render_active_thread_or_empty_state(model, cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),
.children(self.render_last_error(model, cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}

View File

@@ -8,7 +8,7 @@ use crate::message_editor::MessageEditor;
#[derive(IntoElement)]
pub(super) struct ContextPicker<T: PopoverTrigger> {
message_editor: WeakView<MessageEditor>,
message_editor: WeakModel<MessageEditor>,
trigger: T,
}
@@ -22,12 +22,12 @@ struct ContextPickerEntry {
pub(crate) struct ContextPickerDelegate {
all_entries: Vec<ContextPickerEntry>,
filtered_entries: Vec<ContextPickerEntry>,
message_editor: WeakView<MessageEditor>,
message_editor: WeakModel<MessageEditor>,
selected_ix: usize,
}
impl<T: PopoverTrigger> ContextPicker<T> {
pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
pub(crate) fn new(message_editor: WeakModel<MessageEditor>, trigger: T) -> Self {
ContextPicker {
message_editor,
trigger,
@@ -46,18 +46,23 @@ impl PickerDelegate for ContextPickerDelegate {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
model.notify(cx);
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let all_commands = self.all_entries.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
@@ -77,27 +82,27 @@ impl PickerDelegate for ContextPickerDelegate {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.delegate.filtered_entries = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
model.notify(cx);
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
self.message_editor
.update(cx, |_message_editor, _cx| {
.update(cx, |_message_editor, model, _cx| {
println!("Insert context from {}", entry.name);
})
.ok();
cx.emit(DismissEvent);
model.emit(DismissEvent, cx);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
@@ -107,7 +112,8 @@ impl PickerDelegate for ContextPickerDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
_cx: &mut AppContext,
) -> Option<Self::ListItem> {
let entry = self.filtered_entries.get(ix)?;
@@ -118,7 +124,10 @@ impl PickerDelegate for ContextPickerDelegate {
.selected(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
move |cx| {
cx.new_model(|_model, _cx| Tooltip::new(description.clone()))
.into()
}
})
.child(
v_flex()
@@ -150,7 +159,7 @@ impl PickerDelegate for ContextPickerDelegate {
}
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
let entries = vec![
ContextPickerEntry {
name: "directory".into(),
@@ -176,12 +185,13 @@ impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
selected_ix: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
let picker = cx.new_model(|model, cx| {
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()))
});
let handle = self
.message_editor
.update(cx, |this, _| this.context_picker_handle.clone())
.update(cx, |this, model, _| this.context_picker_handle.clone())
.ok();
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(picker.clone()))

View File

@@ -15,18 +15,18 @@ use crate::Chat;
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
editor: Model<Editor>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(thread: Model<Thread>, model: &Model<Self>, cx: &mut AppContext) -> Self {
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything…", cx);
editor: cx.new_model(|model, cx| {
let mut editor = Editor::auto_height(80, model, cx);
editor.set_placeholder_text("Ask anything…", model, cx);
editor
}),
@@ -35,34 +35,35 @@ impl MessageEditor {
}
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
fn chat(&mut self, _: &Chat, model: &Model<Self>, cx: &mut AppContext) {
self.send_to_model(RequestKind::Chat, model, cx);
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<()> {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
model.notify(cx);
return None;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.active_model()?;
let user_message = self.editor.update(cx, |editor, cx| {
let user_message = self.editor.update(cx, |editor, model, cx| {
let text = editor.text(cx);
editor.clear(cx);
text
});
self.thread.update(cx, |thread, cx| {
self.thread.update(cx, |thread, model, cx| {
thread.insert_user_message(user_message, cx);
let mut request = thread.to_completion_request(request_kind, cx);
@@ -93,7 +94,12 @@ impl FocusableView for MessageEditor {
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx);
@@ -107,7 +113,7 @@ impl Render for MessageEditor {
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
cx.view().downgrade(),
model.downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
@@ -160,11 +166,11 @@ impl Render for MessageEditor {
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
KeyBinding::for_action_in(&Chat, &focus_handle, model, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
focus_handle.dispatch_action(&Chat, model, cx);
}),
),
),

View File

@@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
use collections::HashMap;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use gpui::{AppContext, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
@@ -71,7 +71,7 @@ pub struct Thread {
}
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
pub fn new(tools: Arc<ToolWorkingSet>, model: &Model<Self>, _cx: &mut AppContext) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -108,9 +108,14 @@ impl Thread {
self.summary.clone()
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
pub fn set_summary(
&mut self,
summary: impl Into<SharedString>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
model.emit(ThreadEvent::SummaryChanged, cx);
}
pub fn message(&self, id: MessageId) -> Option<&Message> {
@@ -129,15 +134,21 @@ impl Thread {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
self.insert_message(Role::User, text, cx)
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.insert_message(Role::User, text, model, cx)
}
pub fn insert_message(
&mut self,
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
@@ -146,7 +157,7 @@ impl Thread {
text: text.into(),
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
model.emit(ThreadEvent::MessageAdded(id), cx);
}
pub fn to_completion_request(
@@ -200,7 +211,8 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -225,10 +237,13 @@ impl Thread {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
cx.emit(ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
));
model.emit(
cx,
ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
),
);
}
}
}
@@ -259,8 +274,8 @@ impl Thread {
}
thread.touch_updated_at();
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
model.emit(ThreadEvent::StreamedCompletion, cx);
model.notify(cx);
})?;
smol::future::yield_now().await;
@@ -285,25 +300,31 @@ impl Thread {
.update(&mut cx, |_thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
model.emit(ThreadEvent::UsePendingTools, cx);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
model.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired), cx);
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
model.emit(
cx,
ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached),
);
} else {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
SharedString::from(error_message.clone()),
)));
model.emit(
cx,
ThreadEvent::ShowError(ThreadError::Message(SharedString::from(
error_message.clone(),
))),
);
}
}
})
@@ -316,7 +337,7 @@ impl Thread {
});
}
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
pub fn summarize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
@@ -338,7 +359,7 @@ impl Thread {
cache: false,
});
self.pending_summary = cx.spawn(|this, mut cx| {
self.pending_summary = model.spawn(cx, |this, mut cx| {
async move {
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
@@ -355,12 +376,12 @@ impl Thread {
}
}
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryChanged);
model.emit(ThreadEvent::SummaryChanged, cx);
})?;
anyhow::Ok(())
@@ -374,7 +395,8 @@ impl Thread {
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
@@ -400,7 +422,7 @@ impl Thread {
is_error: false,
});
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
model.emit(ThreadEvent::ToolFinished { tool_use_id }, cx);
}
Err(err) => {
tool_results.push(LanguageModelToolResult {

View File

@@ -10,19 +10,20 @@ use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
focus_handle: window.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
@@ -37,8 +38,15 @@ impl FocusableView for ThreadHistory {
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let threads = self
.thread_store
.update(cx, |this, model, cx| this.threads(cx));
v_flex()
.id("thread-history-container")
@@ -85,11 +93,11 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
assistant_panel: WeakModel<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
@@ -98,7 +106,7 @@ impl PastThread {
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
@@ -113,7 +121,7 @@ impl RenderOnce for PastThread {
.unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.update(cx, |this, model, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
@@ -133,7 +141,7 @@ impl RenderOnce for PastThread {
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
.update(cx, |this, model, cx| {
this.delete_thread(&id, cx);
})
.ok();
@@ -146,7 +154,7 @@ impl RenderOnce for PastThread {
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
.update(cx, |this, model, cx| {
this.open_thread(&id, cx);
})
.ok();

View File

@@ -5,7 +5,7 @@ use assistant_tool::{ToolId, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
use gpui::{prelude::*, AppContext, Model, Task};
use project::Project;
use unindent::Unindent;
use util::ResultExt as _;
@@ -28,11 +28,16 @@ impl ThreadStore {
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
let context_server_manager = cx.new_model(|model, cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
model,
cx,
)
});
let mut this = Self {
@@ -52,7 +57,7 @@ impl ThreadStore {
})
}
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
pub fn threads(&self, model: &Model<Self>, cx: &AppContext) -> Vec<Model<Thread>> {
let mut threads = self
.threads
.iter()
@@ -63,28 +68,38 @@ impl ThreadStore {
threads
}
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
pub fn recent_threads(
&self,
limit: usize,
model: &Model<Self>,
cx: &AppContext,
) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
pub fn create_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Thread> {
let thread = cx.new_model(|model, cx| Thread::new(self.tools.clone(), model, cx));
self.threads.push(thread.clone());
thread
}
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
pub fn open_thread(
&self,
id: &ThreadId,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
}
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
pub fn delete_thread(&mut self, id: &ThreadId, model: &Model<Self>, cx: &mut AppContext) {
self.threads.retain(|thread| thread.read(cx).id() != id);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
@@ -96,7 +111,8 @@ impl ThreadStore {
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let tool_working_set = self.tools.clone();
match event {
@@ -153,23 +169,23 @@ impl ThreadStore {
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
fn mock_recent_threads(&mut self, model: &Model<Self>, cx: &mut AppContext) {
use language_model::Role;
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Introduction to quantum computing", model, cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", model, cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", model, cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", model, cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", model, cx);
thread
}));
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
self.threads.push(cx.new_model(|model, cx| {
let mut thread = Thread::new(self.tools.clone(), model, cx);
thread.set_summary("Rust web development and async programming", model, cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", model, cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
```rust
@@ -205,8 +221,8 @@ impl ThreadStore {
actix-web = \"4.0\"
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx);
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), model, cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", model, cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:
@@ -235,7 +251,7 @@ impl ThreadStore {
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), model, cx);
thread
}));
}

View File

@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
use anyhow::Result;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -78,8 +78,9 @@ pub trait SlashCommand: 'static + Send + Sync {
self: Arc<Self>,
arguments: &[String],
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn accepts_arguments(&self) -> bool {
@@ -90,21 +91,27 @@ pub trait SlashCommand: 'static + Send + Sync {
arguments: &[String],
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
context_buffer: BufferSnapshot,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
// TODO: We're just using the `LspAdapterDelegate` here because that is
// what the extension API is already expecting.
//
// It may be that `LspAdapterDelegate` needs a more general name, or
// perhaps another kind of delegate is needed here.
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult>;
}
pub type RenderFoldPlaceholder = Arc<
dyn Send
+ Sync
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
+ Fn(
ElementId,
Arc<dyn Fn(&mut gpui::Window, &mut gpui::AppContext)>,
&mut gpui::Window,
&mut gpui::AppContext,
) -> AnyElement,
>;
#[derive(Debug, PartialEq)]

View File

@@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
@@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand {
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
_workspace: Option<WeakModel<Workspace>>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let command = self.command.clone();
let arguments = arguments.to_owned();
@@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand {
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
_workspace: WeakModel<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<SlashCommandResult> {
let command = self.command.clone();
let arguments = arguments.to_owned();

View File

@@ -4,7 +4,7 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView};
use workspace::Workspace;
pub use crate::tool_registry::*;
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
workspace: WeakView<Workspace>,
cx: &mut WindowContext,
workspace: WeakModel<Workspace>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<String>>;
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use chrono::{Local, Utc};
use gpui::{Task, WeakView, WindowContext};
use gpui::{Task, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -41,8 +41,9 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_workspace: WeakView<workspace::Workspace>,
_cx: &mut WindowContext,
_workspace: WeakModel<workspace::Workspace>,
_window: &mut gpui::Window,
_cx: &mut gpui::AppContext,
) -> Task<Result<String>> {
let input: FileToolInput = match serde_json::from_value(input) {
Ok(input) => input,

View File

@@ -3,8 +3,7 @@ use client::{Client, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, Task, WindowContext,
actions, AppContext, AsyncAppContext, Context as _, Global, Model, SemanticVersion, Task,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
@@ -131,16 +130,16 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|_, action: &Check, cx| check(action, cx));
workspace.register_action(model, |_, action: &Check, cx| check(action, model, cx));
workspace.register_action(|_, action, cx| {
workspace.register_action(model, |_, action, cx| {
view_release_notes(action, cx);
});
})
.detach();
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new_model(|cx| {
let auto_updater = cx.new_model(|model, cx| {
let updater = AutoUpdater::new(version, http_client);
let poll_for_updates = ReleaseChannel::try_global(cx)
@@ -153,7 +152,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
{
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
.then(|| updater.start_polling(model, cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
@@ -172,7 +171,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
pub fn check(_: &Check, cx: &mut WindowContext) {
pub fn check(_: &Check, window: &mut gpui::Window, cx: &mut gpui::AppContext) {
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
drop(cx.prompt(
gpui::PromptLevel::Info,
@@ -201,7 +200,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
updater.update(cx, |updater, model, cx| updater.poll(model, cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
@@ -249,30 +248,30 @@ impl AutoUpdater {
}
}
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.spawn(|this, mut cx| async move {
pub fn start_polling(&self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.spawn(cx, |this, mut cx| async move {
loop {
this.update(&mut cx, |this, cx| this.poll(cx))?;
this.update(&mut cx, |this, model, cx| this.poll(model, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
pub fn poll(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.pending_poll.is_some() || self.status.is_updated() {
return;
}
cx.notify();
model.notify(cx);
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
self.pending_poll = Some(model.spawn(cx, |this, mut cx| async move {
let result = Self::update(this.upgrade()?, cx.clone()).await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_poll = None;
if let Err(error) = result {
log::error!("auto-update failed: error:{:?}", error);
this.status = AutoUpdateStatus::Errored;
cx.notify();
model.notify(cx);
}
})
.ok()
@@ -287,9 +286,9 @@ impl AutoUpdater {
self.status.clone()
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
pub fn dismiss_error(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.status = AutoUpdateStatus::Idle;
cx.notify();
model.notify(cx);
}
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
@@ -432,15 +431,16 @@ impl AutoUpdater {
}
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
cx.notify();
(
this.http_client.clone(),
this.current_version,
ReleaseChannel::try_global(cx),
)
})?;
let (client, current_version, release_channel) =
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Checking;
model.notify(cx);
(
this.http_client.clone(),
this.current_version,
ReleaseChannel::try_global(cx),
)
})?;
let release =
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
@@ -455,16 +455,16 @@ impl AutoUpdater {
};
if !should_download {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
model.notify(cx);
})?;
return Ok(());
}
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Downloading;
cx.notify();
model.notify(cx);
})?;
let temp_dir = tempfile::Builder::new()
@@ -485,9 +485,9 @@ impl AutoUpdater {
let downloaded_asset = temp_dir.path().join(filename);
download_release(&downloaded_asset, release, client, &cx).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
model.notify(cx);
})?;
let binary_path = match OS {
@@ -496,11 +496,11 @@ impl AutoUpdater {
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
model.notify(cx);
})?;
Ok(())

View File

@@ -2,7 +2,7 @@ mod update_notification;
use auto_update::AutoUpdater;
use editor::{Editor, MultiBuffer};
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
use gpui::{actions, prelude::*, AppContext, Model, SharedString, View};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -18,8 +18,8 @@ actions!(auto_update, [ViewReleaseNotesLocally]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
workspace.register_action(model, |workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, model, cx);
});
})
.detach();
@@ -31,7 +31,11 @@ struct ReleaseNotesBody {
release_notes: String,
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
fn view_release_notes_locally(
workspace: &mut Workspace,
model: &Model<Workspace>,
cx: &mut AppContext,
) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
@@ -60,7 +64,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
.with_local_workspace(model, cx, move |_, model, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
@@ -78,27 +82,29 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
let buffer = project.update(cx, |project, model, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer =
cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
let editor = cx.new_model(|model, cx| {
Editor::for_multibuffer(buffer, Some(project), true, model, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
let view: Model<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
model,
cx,
);
workspace.add_item_to_active_pane(
@@ -107,7 +113,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
true,
cx,
);
cx.notify();
model.notify(cx);
})
.log_err();
}
@@ -117,7 +123,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
pub fn notify_of_any_new_update(model: &Model<Workspace>, cx: &mut AppContext) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version();
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
@@ -130,9 +136,9 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|cx| cx.new_model(|_, _| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater.update(cx, |updater, model, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);

View File

@@ -1,6 +1,6 @@
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
div, AppContext, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement,
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakView,
};
use menu::Cancel;
use release_channel::ReleaseChannel;
@@ -12,13 +12,18 @@ use workspace::{
pub struct UpdateNotification {
version: SemanticVersion,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
}
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let app_name = ReleaseChannel::global(cx).display_name();
v_flex()
@@ -37,7 +42,10 @@ impl Render for UpdateNotification {
.id("cancel")
.child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
.on_click(
model
.listener(|this, model, _, cx| this.dismiss(&menu::Cancel, cx)),
),
),
)
.child(
@@ -45,10 +53,10 @@ impl Render for UpdateNotification {
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
crate::view_release_notes_locally(workspace, cx);
.update(cx, |workspace, model, cx| {
crate::view_release_notes_locally(workspace, model, cx);
})
.log_err();
this.dismiss(&menu::Cancel, cx)
@@ -58,11 +66,11 @@ impl Render for UpdateNotification {
}
impl UpdateNotification {
pub fn new(version: SemanticVersion, workspace: WeakView<Workspace>) -> Self {
pub fn new(version: SemanticVersion, workspace: WeakModel<Workspace>) -> Self {
Self { version, workspace }
}
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
pub fn dismiss(&mut self, _: &Cancel, model: &Model<Self>, cx: &mut AppContext) {
model.emit(DismissEvent, cx);
}
}

View File

@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
Subscription, ViewContext,
AppContext, Element, EventEmitter, FocusableView, IntoElement, Model, ParentElement, Render,
StyledText, Subscription,
};
use itertools::Itertools;
use std::cmp;
@@ -37,7 +37,12 @@ impl Breadcrumbs {
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
@@ -64,7 +69,7 @@ impl Render for Breadcrumbs {
}
let highlighted_segments = segments.into_iter().map(|segment| {
let mut text_style = cx.text_style();
let mut text_style = window.text_style();
if let Some(font) = segment.font {
text_style.font_family = font.family;
text_style.font_features = font.features;
@@ -94,23 +99,25 @@ impl Render for Breadcrumbs {
let editor = editor.clone();
move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
outline::toggle(editor, &editor::actions::ToggleOutline, model, cx)
}
}
})
.tooltip(move |cx| {
.tooltip(move |window, cx| {
if let Some(editor) = editor.upgrade() {
let focus_handle = editor.read(cx).focus_handle(cx);
Tooltip::for_action_in(
"Show symbol outline",
&editor::actions::ToggleOutline,
&focus_handle,
window,
cx,
)
} else {
Tooltip::for_action(
"Show symbol outline",
&editor::actions::ToggleOutline,
window,
cx,
)
}
@@ -128,26 +135,30 @@ impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> ToolbarItemLocation {
cx.notify();
model.notify(cx);
self.active_item = None;
let Some(item) = active_pane_item else {
return ToolbarItemLocation::Hidden;
};
let this = cx.view().downgrade();
let this = model.downgrade();
self.subscription = Some(item.subscribe_to_item_events(
cx,
Box::new(move |event, cx| {
if let ItemEvent::UpdateBreadcrumbs = event {
this.update(cx, |this, cx| {
cx.notify();
this.update(cx, |this, model, cx| {
model.notify(cx);
if let Some(active_item) = this.active_item.as_ref() {
cx.emit(ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
))
model.emit(
cx,
ToolbarItemEvent::ChangeLocation(
active_item.breadcrumb_location(cx),
),
)
}
})
.ok();
@@ -158,7 +169,7 @@ impl ToolbarItemView for Breadcrumbs {
item.breadcrumb_location(cx)
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
fn pane_focus_update(&mut self, pane_focused: bool, _: &Model<Self>, _: &mut AppContext) {
self.pane_focused = pane_focused;
}
}

View File

@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
WeakModel,
};
use postage::watch;
use project::Project;
@@ -34,7 +34,7 @@ pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppConte
);
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
cx.set_global(GlobalActiveCall(active_call));
}
@@ -96,7 +96,12 @@ pub struct ActiveCall {
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
model: &Model<Self>,
_: &mut AppContext,
) -> Self {
Self {
room: None,
pending_room_creation: None,
@@ -105,8 +110,8 @@ impl ActiveCall {
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
],
client,
user_store,
@@ -122,22 +127,22 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
.update(&mut cx, |user_store, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
@@ -149,7 +154,7 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
@@ -174,12 +179,13 @@ impl ActiveCall {
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
model.notify(cx);
if self._join_debouncer.running() {
return Task::ready(Ok(()));
@@ -192,20 +198,22 @@ impl ActiveCall {
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
cx.spawn(move |mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
room.update(&mut cx, |room, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
})?
.await?;
@@ -214,8 +222,8 @@ impl ActiveCall {
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let room = model
.spawn(cx, move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
@@ -229,14 +237,16 @@ impl ActiveCall {
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(Some(room.clone()), model, cx)
})?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
@@ -247,18 +257,20 @@ impl ActiveCall {
})
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
this.update(&mut cx, |this, _model, cx| {
this.report_call_event("invite", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
model.notify(cx);
})?;
result
})
@@ -267,7 +279,8 @@ impl ActiveCall {
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
_: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
@@ -291,7 +304,11 @@ impl ActiveCall {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn accept_incoming(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
@@ -313,18 +330,20 @@ impl ActiveCall {
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
let call = self
.incoming_call
.0
@@ -341,13 +360,14 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
room.update(cx, |room, model, cx| room.clear_state(cx));
}
}
@@ -361,27 +381,29 @@ impl ActiveCall {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
} else {
Task::ready(Ok(()))
}
@@ -390,11 +412,12 @@ impl ActiveCall {
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
@@ -403,11 +426,14 @@ impl ActiveCall {
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, cx)
})
} else {
Err(anyhow!("no active call"))
}
@@ -420,12 +446,13 @@ impl ActiveCall {
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
}
}
Task::ready(Ok(()))
@@ -434,26 +461,29 @@ impl ActiveCall {
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
model.notify(cx);
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
model.observe(&room, cx, |this, room, model, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
this.set_room(None, model, cx).detach_and_log_err(cx)
}
cx.notify();
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
@@ -461,8 +491,10 @@ impl ActiveCall {
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, cx)
})
}
} else {
self.room = None;

View File

@@ -13,9 +13,7 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
use language::LanguageRegistry;
#[cfg(not(target_os = "windows"))]
use livekit::{
@@ -121,11 +119,12 @@ impl Room {
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
spawn_room_connection(livekit_connection_info, cx);
spawn_room_connection(livekit_connection_info, model, cx);
let maintain_connection = cx.spawn({
let maintain_connection = model.spawn(cx, {
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
@@ -147,11 +146,11 @@ impl Room {
pending_participants: Default::default(),
pending_call_count: 0,
client_subscriptions: vec![
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
],
_subscriptions: vec![
cx.on_release(Self::released),
cx.on_app_quit(Self::app_will_quit),
model.on_release(cx, Self::released),
model.on_app_quit(cx, Self::app_will_quit),
],
leave_when_empty: false,
pending_room_update: None,
@@ -174,13 +173,14 @@ impl Room {
cx.spawn(move |mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
let mut room = Self::new(
room_proto.id,
None,
response.live_kit_connection_info,
client,
user_store,
model,
cx,
);
if let Some(participant) = room_proto.participants.first() {
@@ -191,8 +191,8 @@ impl Room {
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
.update(&mut cx, |room, model, cx| {
room.share_project(initial_project.clone(), model, cx)
})?
.await?;
Some(initial_project_id)
@@ -201,9 +201,9 @@ impl Room {
};
let did_join = room
.update(&mut cx, |room, cx| {
.update(&mut cx, |room, model, cx| {
room.leave_when_empty = true;
room.call(called_user_id, initial_project_id, cx)
room.call(called_user_id, initial_project_id, model, cx)
})?
.await;
match did_join {
@@ -251,7 +251,11 @@ impl Room {
}
}
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
fn app_will_quit(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl Future<Output = ()> {
let task = if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(cx.background_executor().spawn(async move {
@@ -279,19 +283,20 @@ impl Room {
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
Self::new(
room_proto.id,
response.channel_id.map(ChannelId),
response.live_kit_connection_info,
client,
user_store,
model,
cx,
)
})?;
room.update(&mut cx, |room, cx| {
room.update(&mut cx, |room, model, cx| {
room.leave_when_empty = room.channel_id.is_none();
room.apply_room_update(room_proto, cx)?;
room.apply_room_update(room_proto, model, cx)?;
anyhow::Ok(())
})??;
Ok(room)
@@ -305,8 +310,8 @@ impl Room {
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
self.leave_internal(cx)
}
@@ -330,16 +335,16 @@ impl Room {
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
project.update(cx, |project, model, cx| {
project.unshare(model, cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
project.update(cx, |project, model, cx| {
project.disconnected_from_host(model, cx);
project.close(model, cx);
});
}
}
@@ -369,9 +374,9 @@ impl Room {
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
model.notify(cx);
})?;
// Wait for client to re-establish a connection to the server.
@@ -385,7 +390,8 @@ impl Room {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
{
Ok(task) => {
if task.await.log_err().is_some() {
return true;
@@ -434,14 +440,15 @@ impl Room {
// we leave the room and return an error.
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
.await?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
let mut projects = HashMap::default();
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
@@ -489,27 +496,29 @@ impl Room {
rejoined_projects,
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
this.apply_room_update(room_proto, model, cx)?;
for reshared_project in response.reshared_projects {
if let Some(project) = projects.get(&reshared_project.id) {
project.update(cx, |project, cx| {
project.reshared(reshared_project, cx).log_err();
project.update(cx, |project, model, cx| {
project.reshared(reshared_project, model, cx).log_err();
});
}
}
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, message_id, cx).log_err();
project.update(cx, |project, model, cx| {
project
.rejoined(rejoined_project, message_id, model, cx)
.log_err();
});
}
}
@@ -567,12 +576,13 @@ impl Room {
&mut self,
user_id: u64,
role: proto::ChannelRole,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
let room_id = self.id;
let role = role.into();
cx.spawn(|_, _| async move {
cx.spawn(|_| async move {
client
.request(proto::SetRoomParticipantRole {
room_id,
@@ -644,19 +654,26 @@ impl Room {
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
this.update(&mut cx, |this, model, cx| {
this.apply_room_update(room, model, cx)
})?
}
fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext<Self>) -> Result<()> {
fn apply_room_update(
&mut self,
room: proto::Room,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
log::trace!(
"client {:?}. room update: {:?}",
self.client.user_id(),
&room
);
self.pending_room_update = Some(self.start_room_connection(room, cx));
self.pending_room_update = Some(self.start_room_connection(room, model, cx));
cx.notify();
model.notify(cx);
Ok(())
}
@@ -675,7 +692,8 @@ impl Room {
fn start_room_connection(
&self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<()> {
Task::ready(())
}
@@ -684,7 +702,8 @@ impl Room {
fn start_room_connection(
&self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
@@ -706,17 +725,17 @@ impl Room {
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
self.user_store.update(cx, move |user_store, model, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(pending_participant_user_ids, cx),
user_store.get_users(remote_participant_user_ids, model, cx),
user_store.get_users(pending_participant_user_ids, model, cx),
)
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
@@ -728,18 +747,20 @@ impl Room {
if role == proto::ChannelRole::Guest {
for project in mem::take(&mut this.shared_projects) {
if let Some(project) = project.upgrade() {
this.unshare_project(project, cx).log_err();
this.unshare_project(project, model, cx).log_err();
}
}
this.local_participant.projects.clear();
if let Some(livekit_room) = &mut this.live_kit {
livekit_room.stop_publishing(cx);
livekit_room.stop_publishing(model, cx);
}
}
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| project.set_role(role, cx));
project.update(cx, |project, model, cx| {
project.set_role(role, model, cx)
});
true
} else {
false
@@ -778,20 +799,23 @@ impl Room {
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
model.emit(
Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
},
cx,
);
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.update(cx, |project, model, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(cx);
project.disconnected_from_host(model, cx);
false
} else {
true
@@ -801,9 +825,12 @@ impl Room {
false
}
});
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
},
cx,
);
}
let role = participant.role();
@@ -820,9 +847,12 @@ impl Room {
{
remote_participant.location = location;
remote_participant.role = role;
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
model.emit(
Event::ParticipantLocationChanged {
participant_id: peer_id,
},
cx,
);
}
} else {
this.remote_participants.insert(
@@ -857,6 +887,7 @@ impl Room {
publication,
participant: livekit_participant.clone(),
},
model,
cx,
)
.warn_on_err();
@@ -872,9 +903,12 @@ impl Room {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: project.id,
},
cx,
);
}
false
}
@@ -912,21 +946,21 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
this.leave(cx).detach();
this.leave(model, cx).detach();
}
this.user_store.update(cx, |user_store, cx| {
this.user_store.update(cx, |user_store, model, cx| {
let participant_indices_by_user_id = this
.remote_participants
.iter()
.map(|(user_id, participant)| (*user_id, participant.participant_index))
.collect();
user_store.set_participant_indices(participant_indices_by_user_id, cx);
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
model.notify(cx);
})
.ok();
})
@@ -935,7 +969,8 @@ impl Room {
fn livekit_room_updated(
&mut self,
event: RoomEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
log::trace!(
"client {:?}. livekit event: {:?}",
@@ -963,17 +998,23 @@ impl Room {
}
match track {
livekit::track::RemoteTrack::Audio(track) => {
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
let stream = play_remote_audio_track(&track, cx.background_executor())?;
participant.audio_tracks.insert(track_id, (track, stream));
participant.muted = publication.is_muted();
}
livekit::track::RemoteTrack::Video(track) => {
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
participant.video_tracks.insert(track_id, track);
}
}
@@ -994,15 +1035,21 @@ impl Room {
livekit::track::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
participant.muted = true;
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
livekit::track::RemoteTrack::Video(track) => {
participant.video_tracks.remove(&track.sid());
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
}
}
@@ -1080,12 +1127,12 @@ impl Room {
#[cfg(not(target_os = "windows"))]
RoomEvent::Disconnected { reason } => {
log::info!("disconnected from room: {reason:?}");
self.leave(cx).detach_and_log_err(cx);
self.leave(model, cx).detach_and_log_err(cx);
}
_ => {}
}
cx.notify();
model.notify(cx);
Ok(())
}
@@ -1113,17 +1160,18 @@ impl Room {
&mut self,
called_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
@@ -1131,10 +1179,10 @@ impl Room {
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx).detach_and_log_err(cx);
this.leave(model, cx).detach_and_log_err(cx);
}
})?;
result?;
@@ -1147,16 +1195,17 @@ impl Room {
id: u64,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move {
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
model.spawn(cx, move |this, mut cx| async move {
let project =
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, _, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected(cx)
@@ -1173,7 +1222,8 @@ impl Room {
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
@@ -1185,19 +1235,19 @@ impl Room {
is_ssh_project: project.read(cx).is_via_ssh(),
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
project.update(&mut cx, |project, model, cx| {
project.shared(response.project_id, model, cx)
})??;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
this.set_location(Some(&project), model, cx)
} else {
Task::ready(Ok(()))
}
@@ -1211,7 +1261,8 @@ impl Room {
pub(crate) fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
@@ -1219,10 +1270,11 @@ impl Room {
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, cx| this.unshare(cx))?;
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
if self.local_participant.active_project == Some(project.downgrade()) {
self.set_location(Some(&project), cx).detach_and_log_err(cx);
self.set_location(Some(&project), model, cx)
.detach_and_log_err(cx);
}
Ok(())
}
@@ -1230,7 +1282,8 @@ impl Room {
pub(crate) fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@@ -1254,7 +1307,7 @@ impl Room {
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
model.notify(cx);
cx.background_executor().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
@@ -1324,13 +1377,21 @@ impl Room {
}
#[cfg(target_os = "windows")]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Windows is not supported yet")))
}
#[cfg(not(target_os = "windows"))]
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1338,13 +1399,13 @@ impl Room {
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
(live_kit.room.local_participant(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let (track, stream) = capture_local_audio_track(cx.background_executor())?.await;
let publication = participant
@@ -1357,7 +1418,7 @@ impl Room {
)
.await
.map_err(|error| anyhow!("failed to publish track: {error}"));
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1388,7 +1449,7 @@ impl Room {
track_publication: publication,
_stream: Box::new(stream),
};
cx.notify();
model.notify(cx);
}
Ok(())
}
@@ -1397,7 +1458,7 @@ impl Room {
Ok(())
} else {
live_kit.microphone_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1407,12 +1468,12 @@ impl Room {
}
#[cfg(target_os = "windows")]
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Windows is not supported yet")))
}
#[cfg(not(target_os = "windows"))]
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1423,7 +1484,7 @@ impl Room {
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
(live_kit.room.local_participant(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
@@ -1431,7 +1492,7 @@ impl Room {
let sources = cx.screen_capture_sources();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
@@ -1449,7 +1510,7 @@ impl Room {
.await
.map_err(|error| anyhow!("error publishing screen track {error:?}"));
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1477,7 +1538,7 @@ impl Room {
track_publication: publication,
_stream: Box::new(stream),
};
cx.notify();
model.notify(cx);
}
Audio::play_sound(Sound::StartScreenshare, cx);
@@ -1488,7 +1549,7 @@ impl Room {
Ok(())
} else {
live_kit.screen_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1497,7 +1558,7 @@ impl Room {
})
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When unmuting, undeafen if the user was deafened before.
let was_deafened = live_kit.deafened;
@@ -1513,17 +1574,17 @@ impl Room {
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, cx) {
if let Some(task) = self.set_mute(muted, model, cx) {
task.detach_and_log_err(cx);
}
if should_undeafen {
self.set_deafened(false, cx);
self.set_deafened(false, model, cx);
}
}
}
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When deafening, mute the microphone if it was not already muted.
// When un-deafening, unmute the microphone, unless it was explicitly muted.
@@ -1531,17 +1592,17 @@ impl Room {
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
self.set_deafened(deafened, cx);
self.set_deafened(deafened, model, cx);
if should_change_mute {
if let Some(task) = self.set_mute(deafened, cx) {
if let Some(task) = self.set_mute(deafened, model, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
@@ -1553,7 +1614,7 @@ impl Room {
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
model.notify(cx);
Ok(())
}
LocalTrack::Published {
@@ -1566,7 +1627,7 @@ impl Room {
cx.background_executor()
.spawn(async move { local_participant.unpublish_track(&sid).await })
.detach_and_log_err(cx);
cx.notify();
model.notify(cx);
}
Audio::play_sound(Sound::StopScreenshare, cx);
Ok(())
@@ -1574,11 +1635,16 @@ impl Room {
}
}
fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext<Self>) -> Option<()> {
fn set_deafened(
&mut self,
deafened: bool,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<()> {
#[cfg(not(target_os = "windows"))]
{
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
for (_, participant) in live_kit.room.remote_participants() {
for (_, publication) in participant.track_publications() {
if publication.kind() == TrackKind::Audio {
@@ -1594,10 +1660,11 @@ impl Room {
fn set_mute(
&mut self,
should_mute: bool,
cx: &mut ModelContext<Room>,
model: &Model<Room>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
if should_mute {
Audio::play_sound(Sound::Mute, cx);
@@ -1610,7 +1677,7 @@ impl Room {
if should_mute {
None
} else {
Some(self.share_microphone(cx))
Some(self.share_microphone(model, cx))
}
}
LocalTrack::Pending { .. } => None,
@@ -1634,59 +1701,62 @@ impl Room {
#[cfg(target_os = "windows")]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
cx: &mut ModelContext<'_, Room>,
model: &Model<Room>,
cx: &mut AppContext,
) {
}
#[cfg(not(target_os = "windows"))]
fn spawn_room_connection(
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
cx: &mut ModelContext<'_, Room>,
model: &Model<Room>,
cx: &mut AppContext,
) {
if let Some(connection_info) = livekit_connection_info {
cx.spawn(|this, mut cx| async move {
let (room, mut events) = livekit::Room::connect(
&connection_info.server_url,
&connection_info.token,
RoomOptions::default(),
)
.await?;
model
.spawn(cx, |this, mut cx| async move {
let (room, mut events) = livekit::Room::connect(
&connection_info.server_url,
&connection_info.token,
RoomOptions::default(),
)
.await?;
this.update(&mut cx, |this, cx| {
let _handle_updates = cx.spawn(|this, mut cx| async move {
while let Some(event) = events.recv().await {
if this
.update(&mut cx, |this, cx| {
this.livekit_room_updated(event, cx).warn_on_err();
})
.is_err()
{
break;
this.update(&mut cx, |this, model, cx| {
let _handle_updates = model.spawn(cx, |this, mut cx| async move {
while let Some(event) = events.recv().await {
if this
.update(&mut cx, |this, model, cx| {
this.livekit_room_updated(event, model, cx).warn_on_err();
})
.is_err()
{
break;
}
}
});
let muted_by_user = Room::mute_on_join(cx);
this.live_kit = Some(LiveKitRoom {
room: Arc::new(room),
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user,
deafened: false,
speaking: false,
_handle_updates,
});
if !muted_by_user && this.can_use_microphone(cx) {
this.share_microphone(model, cx)
} else {
Task::ready(Ok(()))
}
});
let muted_by_user = Room::mute_on_join(cx);
this.live_kit = Some(LiveKitRoom {
room: Arc::new(room),
screen_track: LocalTrack::None,
microphone_track: LocalTrack::None,
next_publish_id: 0,
muted_by_user,
deafened: false,
speaking: false,
_handle_updates,
});
if !muted_by_user && this.can_use_microphone(cx) {
this.share_microphone(cx)
} else {
Task::ready(Ok(()))
}
})?
.await
})
.detach_and_log_err(cx);
})?
.await
})
.detach_and_log_err(cx);
}
}
@@ -1704,17 +1774,17 @@ struct LiveKitRoom {
impl LiveKitRoom {
#[cfg(target_os = "windows")]
fn stop_publishing(&mut self, _cx: &mut ModelContext<Room>) {}
fn stop_publishing(&mut self, model: &Model<Room>, _cx: &mut AppContext) {}
#[cfg(not(target_os = "windows"))]
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
let mut tracks_to_unpublish = Vec::new();
if let LocalTrack::Published {
track_publication, ..
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
{
tracks_to_unpublish.push(track_publication.sid());
cx.notify();
model.notify(cx);
}
if let LocalTrack::Published {
@@ -1722,7 +1792,7 @@ impl LiveKitRoom {
} = mem::replace(&mut self.screen_track, LocalTrack::None)
{
tracks_to_unpublish.push(track_publication.sid());
cx.notify();
model.notify(cx);
}
let participant = self.room.local_participant();

View File

@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
WeakModel,
};
use postage::watch;
use project::Project;
@@ -27,7 +27,7 @@ impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
cx.set_global(GlobalActiveCall(active_call));
}
@@ -89,7 +89,12 @@ pub struct ActiveCall {
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
Self {
room: None,
pending_room_creation: None,
@@ -98,8 +103,8 @@ impl ActiveCall {
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
],
client,
user_store,
@@ -115,22 +120,22 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
.update(&mut cx, |user_store, model, cx| {
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
.update(&mut cx, |user_store, model, cx| {
user_store.get_user(envelope.payload.calling_user_id, model, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
@@ -142,7 +147,7 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
@@ -167,12 +172,13 @@ impl ActiveCall {
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
model.notify(cx);
if self._join_debouncer.running() {
return Task::ready(Ok(()));
@@ -185,20 +191,22 @@ impl ActiveCall {
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
cx.spawn(move |mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
room.update(&mut cx, |room, model, cx| {
room.share_project(initial_project, model, cx)
})?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
room.update(&mut cx, move |room, model, cx| {
room.call(called_user_id, initial_project_id, model, cx)
})?
.await?;
@@ -207,8 +215,8 @@ impl ActiveCall {
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let room = model
.spawn(cx, move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
@@ -222,14 +230,16 @@ impl ActiveCall {
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
this.update(&mut cx, |this, model, cx| {
this.set_room(Some(room.clone()), model, cx)
})?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
@@ -240,18 +250,20 @@ impl ActiveCall {
})
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("invite", cx)
})?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
model.notify(cx);
})?;
result
})
@@ -260,7 +272,8 @@ impl ActiveCall {
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
@@ -284,7 +297,11 @@ impl ActiveCall {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn accept_incoming(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
@@ -306,18 +323,20 @@ impl ActiveCall {
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
let call = self
.incoming_call
.0
@@ -334,13 +353,14 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
room.update(cx, |room, model, cx| room.clear_state(cx));
}
}
@@ -354,27 +374,29 @@ impl ActiveCall {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.set_room(room.clone(), model, cx)
})?
.await?;
this.update(&mut cx, |this, model, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
model.emit(Event::RoomLeft { channel_id }, cx);
room.update(cx, |room, model, cx| room.leave(model, cx))
} else {
Task::ready(Ok(()))
}
@@ -383,11 +405,12 @@ impl ActiveCall {
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
@@ -396,11 +419,14 @@ impl ActiveCall {
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
room.update(cx, |room, model, cx| {
room.unshare_project(project, model, cx)
})
} else {
Err(anyhow!("no active call"))
}
@@ -413,12 +439,13 @@ impl ActiveCall {
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
}
}
Task::ready(Ok(()))
@@ -427,26 +454,29 @@ impl ActiveCall {
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
model.notify(cx);
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
model.observe(&room, cx, |this, room, model, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
this.set_room(None, model, cx).detach_and_log_err(cx);
}
cx.notify();
model.notify(cx);
}),
model.subscribe(&room, cx, |_, _, event, model, cx| {
model.emit(event.clone(), cx)
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
@@ -454,8 +484,10 @@ impl ActiveCall {
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
model.emit(Event::RoomJoined { channel_id }, cx);
room.update(cx, |room, model, cx| {
room.set_location(location.as_ref(), model, cx)
})
}
} else {
self.room = None;

View File

@@ -11,9 +11,7 @@ use client::{
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
use language::LanguageRegistry;
use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
use postage::{sink::Sink, stream::Stream, watch};
@@ -110,14 +108,15 @@ impl Room {
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = livekit_client_macos::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
let _maintain_room = cx.spawn(|this, mut cx| async move {
let _maintain_room = model.spawn(cx, |this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade() {
this
@@ -126,14 +125,14 @@ impl Room {
};
if status == livekit_client_macos::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
this.update(&mut cx, |this, model, cx| this.leave(model, cx).log_err())
.ok();
break;
}
}
});
let _handle_updates = cx.spawn({
let _handle_updates = model.spawn(cx, {
let room = room.clone();
move |this, mut cx| async move {
let mut updates = room.updates();
@@ -144,8 +143,8 @@ impl Room {
break;
};
this.update(&mut cx, |this, cx| {
this.live_kit_room_updated(update, cx).log_err()
this.update(&mut cx, |this, model, cx| {
this.live_kit_room_updated(update, model, cx).log_err()
})
.ok();
}
@@ -153,21 +152,22 @@ impl Room {
});
let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if this.can_use_microphone(cx) {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
model
.spawn(cx, |this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, model, cx| {
if this.can_use_microphone(cx) {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(model, cx);
}
}
}
}
Task::ready(Ok(()))
})?
.await
})
.detach_and_log_err(cx);
Task::ready(Ok(()))
})?
.await
})
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
@@ -184,7 +184,7 @@ impl Room {
None
};
let maintain_connection = cx.spawn({
let maintain_connection = model.spawn(cx, {
let client = client.clone();
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
});
@@ -206,11 +206,11 @@ impl Room {
pending_participants: Default::default(),
pending_call_count: 0,
client_subscriptions: vec![
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
],
_subscriptions: vec![
cx.on_release(Self::released),
cx.on_app_quit(Self::app_will_quit),
model.on_release(cx, Self::released),
model.on_app_quit(cx, Self::app_will_quit),
],
leave_when_empty: false,
pending_room_update: None,
@@ -233,13 +233,14 @@ impl Room {
cx.spawn(move |mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
let mut room = Self::new(
room_proto.id,
None,
response.live_kit_connection_info,
client,
user_store,
model,
cx,
);
if let Some(participant) = room_proto.participants.first() {
@@ -250,8 +251,8 @@ impl Room {
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
.update(&mut cx, |room, model, cx| {
room.share_project(initial_project.clone(), model, cx)
})?
.await?;
Some(initial_project_id)
@@ -260,9 +261,9 @@ impl Room {
};
let did_join = room
.update(&mut cx, |room, cx| {
.update(&mut cx, |room, model, cx| {
room.leave_when_empty = true;
room.call(called_user_id, initial_project_id, cx)
room.call(called_user_id, initial_project_id, model, cx)
})?
.await;
match did_join {
@@ -310,7 +311,11 @@ impl Room {
}
}
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
fn app_will_quit(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl Future<Output = ()> {
let task = if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(cx.background_executor().spawn(async move {
@@ -338,19 +343,20 @@ impl Room {
mut cx: AsyncAppContext,
) -> Result<Model<Self>> {
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new_model(|cx| {
let room = cx.new_model(|model, cx| {
Self::new(
room_proto.id,
response.channel_id.map(ChannelId),
response.live_kit_connection_info,
client,
user_store,
model,
cx,
)
})?;
room.update(&mut cx, |room, cx| {
room.update(&mut cx, |room, model, cx| {
room.leave_when_empty = room.channel_id.is_none();
room.apply_room_update(room_proto, cx)?;
room.apply_room_update(room_proto, model, cx)?;
anyhow::Ok(())
})??;
Ok(room)
@@ -364,8 +370,8 @@ impl Room {
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
model.notify(cx);
self.leave_internal(cx)
}
@@ -389,16 +395,16 @@ impl Room {
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
project.update(cx, |project, model, cx| {
project.unshare(model, cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
project.update(cx, |project, model, cx| {
project.disconnected_from_host(model, cx);
project.close(model, cx);
});
}
}
@@ -428,9 +434,9 @@ impl Room {
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
model.notify(cx);
})?;
// Wait for client to re-establish a connection to the server.
@@ -444,7 +450,8 @@ impl Room {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade() else { break };
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
{
Ok(task) => {
if task.await.log_err().is_some() {
return true;
@@ -493,14 +500,15 @@ impl Room {
// we leave the room and return an error.
if let Some(this) = this.upgrade() {
log::info!("reconnection failed, leaving room");
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
.await?;
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
let mut projects = HashMap::default();
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
@@ -548,27 +556,29 @@ impl Room {
rejoined_projects,
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
this.apply_room_update(room_proto, model, cx)?;
for reshared_project in response.reshared_projects {
if let Some(project) = projects.get(&reshared_project.id) {
project.update(cx, |project, cx| {
project.reshared(reshared_project, cx).log_err();
project.update(cx, |project, model, cx| {
project.reshared(reshared_project, model, cx).log_err();
});
}
}
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, message_id, cx).log_err();
project.update(cx, |project, model, cx| {
project
.rejoined(rejoined_project, message_id, model, cx)
.log_err();
});
}
}
@@ -626,12 +636,13 @@ impl Room {
&mut self,
user_id: u64,
role: proto::ChannelRole,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
let room_id = self.id;
let role = role.into();
cx.spawn(|_, _| async move {
cx.spawn(|_| async move {
client
.request(proto::SetRoomParticipantRole {
room_id,
@@ -703,13 +714,16 @@ impl Room {
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
this.update(&mut cx, |this, model, cx| {
this.apply_room_update(room, model, cx)
})?
}
fn apply_room_update(
&mut self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
@@ -731,18 +745,18 @@ impl Room {
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
self.user_store.update(cx, move |user_store, model, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(pending_participant_user_ids, cx),
user_store.get_users(remote_participant_user_ids, model, cx),
user_store.get_users(pending_participant_user_ids, model, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
self.pending_room_update = Some(model.spawn(cx, |this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
@@ -754,18 +768,20 @@ impl Room {
if role == proto::ChannelRole::Guest {
for project in mem::take(&mut this.shared_projects) {
if let Some(project) = project.upgrade() {
this.unshare_project(project, cx).log_err();
this.unshare_project(project, model, cx).log_err();
}
}
this.local_participant.projects.clear();
if let Some(live_kit_room) = &mut this.live_kit {
live_kit_room.stop_publishing(cx);
live_kit_room.stop_publishing(model, cx);
}
}
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| project.set_role(role, cx));
project.update(cx, |project, model, cx| {
project.set_role(role, model, cx)
});
true
} else {
false
@@ -799,20 +815,23 @@ impl Room {
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
model.emit(
Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
},
cx,
);
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
project.update(cx, |project, cx| {
project.update(cx, |project, model, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(cx);
project.disconnected_from_host(model, cx);
false
} else {
true
@@ -822,9 +841,12 @@ impl Room {
false
}
});
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
},
cx,
);
}
let role = participant.role();
@@ -841,9 +863,12 @@ impl Room {
{
remote_participant.location = location;
remote_participant.role = role;
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
model.emit(
Event::ParticipantLocationChanged {
participant_id: peer_id,
},
cx,
);
}
} else {
this.remote_participants.insert(
@@ -876,6 +901,7 @@ impl Room {
for track in video_tracks {
this.live_kit_room_updated(
RoomUpdate::SubscribedToRemoteVideoTrack(track),
model,
cx,
)
.log_err();
@@ -889,6 +915,7 @@ impl Room {
track.clone(),
publication.clone(),
),
model,
cx,
)
.log_err();
@@ -902,9 +929,12 @@ impl Room {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
model.emit(
Event::RemoteProjectUnshared {
project_id: project.id,
},
cx,
);
}
false
}
@@ -942,26 +972,26 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
this.leave(cx).detach();
this.leave(model, cx).detach();
}
this.user_store.update(cx, |user_store, cx| {
this.user_store.update(cx, |user_store, model, cx| {
let participant_indices_by_user_id = this
.remote_participants
.iter()
.map(|(user_id, participant)| (*user_id, participant.participant_index))
.collect();
user_store.set_participant_indices(participant_indices_by_user_id, cx);
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
model.notify(cx);
})
.ok();
}));
cx.notify();
model.notify(cx);
Ok(())
}
@@ -979,7 +1009,8 @@ impl Room {
fn live_kit_room_updated(
&mut self,
update: RoomUpdate,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
match update {
RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
@@ -990,9 +1021,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.video_tracks.insert(track_id.clone(), track);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::UnsubscribedFromRemoteVideoTrack {
@@ -1005,9 +1039,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.video_tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteVideoTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::ActiveSpeakersChanged { speakers } => {
@@ -1061,9 +1098,12 @@ impl Room {
participant.audio_tracks.insert(track_id.clone(), track);
participant.muted = publication.is_muted();
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::UnsubscribedFromRemoteAudioTrack {
@@ -1076,9 +1116,12 @@ impl Room {
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.audio_tracks.remove(&track_id);
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
model.emit(
Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
},
cx,
);
}
RoomUpdate::LocalAudioTrackUnpublished { publication } => {
@@ -1104,7 +1147,7 @@ impl Room {
}
}
cx.notify();
model.notify(cx);
Ok(())
}
@@ -1132,17 +1175,18 @@ impl Room {
&mut self,
called_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
@@ -1150,10 +1194,10 @@ impl Room {
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx).detach_and_log_err(cx);
this.leave(model, cx).detach_and_log_err(cx);
}
})?;
result?;
@@ -1166,16 +1210,17 @@ impl Room {
id: u64,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move {
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
model.spawn(cx, move |this, mut cx| async move {
let project =
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade() {
!project.read(cx).is_disconnected(cx)
@@ -1192,7 +1237,8 @@ impl Room {
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
@@ -1204,19 +1250,19 @@ impl Room {
is_ssh_project: project.read(cx).is_via_ssh(),
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project.shared(response.project_id, cx)
project.update(&mut cx, |project, model, cx| {
project.shared(response.project_id, model, cx)
})??;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
this.set_location(Some(&project), model, cx)
} else {
Task::ready(Ok(()))
}
@@ -1230,7 +1276,8 @@ impl Room {
pub(crate) fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
@@ -1238,10 +1285,11 @@ impl Room {
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, cx| this.unshare(cx))?;
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
if self.local_participant.active_project == Some(project.downgrade()) {
self.set_location(Some(&project), cx).detach_and_log_err(cx);
self.set_location(Some(&project), model, cx)
.detach_and_log_err(cx);
}
Ok(())
}
@@ -1249,7 +1297,8 @@ impl Room {
pub(crate) fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@@ -1273,7 +1322,7 @@ impl Room {
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
model.notify(cx);
cx.background_executor().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
@@ -1334,7 +1383,11 @@ impl Room {
}
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_microphone(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
@@ -1342,18 +1395,18 @@ impl Room {
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.microphone_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
publish_id
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let publish_track = async {
let track = LocalAudioTrack::create();
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, _| {
.update(&mut cx, |this, model, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_audio_track(track))
@@ -1364,7 +1417,7 @@ impl Room {
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1392,7 +1445,7 @@ impl Room {
live_kit.microphone_track = LocalTrack::Published {
track_publication: publication,
};
cx.notify();
model.notify(cx);
}
Ok(())
}
@@ -1401,7 +1454,7 @@ impl Room {
Ok(())
} else {
live_kit.microphone_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1410,7 +1463,7 @@ impl Room {
})
}
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
} else if self.is_screen_sharing() {
@@ -1420,13 +1473,13 @@ impl Room {
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = LocalTrack::Pending { publish_id };
cx.notify();
model.notify(cx);
(live_kit.room.display_sources(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let publish_track = async {
let displays = displays.await?;
let display = displays
@@ -1435,7 +1488,7 @@ impl Room {
let track = LocalVideoTrack::screen_share_for_display(display);
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, _| {
.update(&mut cx, |this, model, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_video_track(track))
@@ -1447,7 +1500,7 @@ impl Room {
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
.update(&mut cx, |this, model, cx| {
let live_kit = this
.live_kit
.as_mut()
@@ -1470,7 +1523,7 @@ impl Room {
live_kit.screen_track = LocalTrack::Published {
track_publication: publication,
};
cx.notify();
model.notify(cx);
}
Audio::play_sound(Sound::StartScreenshare, cx);
@@ -1482,7 +1535,7 @@ impl Room {
Ok(())
} else {
live_kit.screen_track = LocalTrack::None;
cx.notify();
model.notify(cx);
Err(error)
}
}
@@ -1491,7 +1544,7 @@ impl Room {
})
}
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When unmuting, undeafen if the user was deafened before.
let was_deafened = live_kit.deafened;
@@ -1507,19 +1560,19 @@ impl Room {
let muted = live_kit.muted_by_user;
let should_undeafen = was_deafened && !live_kit.deafened;
if let Some(task) = self.set_mute(muted, cx) {
if let Some(task) = self.set_mute(muted, model, cx) {
task.detach_and_log_err(cx);
}
if should_undeafen {
if let Some(task) = self.set_deafened(false, cx) {
if let Some(task) = self.set_deafened(false, model, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(live_kit) = self.live_kit.as_mut() {
// When deafening, mute the microphone if it was not already muted.
// When un-deafening, unmute the microphone, unless it was explicitly muted.
@@ -1527,19 +1580,19 @@ impl Room {
live_kit.deafened = deafened;
let should_change_mute = !live_kit.muted_by_user;
if let Some(task) = self.set_deafened(deafened, cx) {
if let Some(task) = self.set_deafened(deafened, model, cx) {
task.detach_and_log_err(cx);
}
if should_change_mute {
if let Some(task) = self.set_mute(deafened, cx) {
if let Some(task) = self.set_mute(deafened, model, cx) {
task.detach_and_log_err(cx);
}
}
}
}
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
@@ -1551,14 +1604,14 @@ impl Room {
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
model.notify(cx);
Ok(())
}
LocalTrack::Published {
track_publication, ..
} => {
live_kit.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
Audio::play_sound(Sound::StopScreenshare, cx);
Ok(())
@@ -1569,10 +1622,11 @@ impl Room {
fn set_deafened(
&mut self,
deafened: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
let mut track_updates = Vec::new();
for participant in self.remote_participants.values() {
@@ -1603,10 +1657,11 @@ impl Room {
fn set_mute(
&mut self,
should_mute: bool,
cx: &mut ModelContext<Room>,
model: &Model<Room>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
let live_kit = self.live_kit.as_mut()?;
cx.notify();
model.notify(cx);
if should_mute {
Audio::play_sound(Sound::Mute, cx);
@@ -1619,7 +1674,7 @@ impl Room {
if should_mute {
None
} else {
Some(self.share_microphone(cx))
Some(self.share_microphone(model, cx))
}
}
LocalTrack::Pending { .. } => None,
@@ -1654,13 +1709,13 @@ struct LiveKitRoom {
}
impl LiveKitRoom {
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
if let LocalTrack::Published {
track_publication, ..
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
{
self.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
}
if let LocalTrack::Published {
@@ -1668,7 +1723,7 @@ impl LiveKitRoom {
} = mem::replace(&mut self.screen_track, LocalTrack::None)
{
self.room.unpublish_track(track_publication);
cx.notify();
model.notify(cx);
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore};
use anyhow::Result;
use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},
@@ -62,17 +62,21 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let buffer = cx.new_model(|cx| {
let buffer = cx.new_model(|model, cx| {
let capability = channel_store.read(cx).channel_capability(channel.id);
language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text)
})?;
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
buffer.update(&mut cx, |buffer, model, cx| {
buffer.apply_ops(operations, model, cx)
})?;
let subscription = client.subscribe_to_entity(channel.id.0)?;
anyhow::Ok(cx.new_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
cx.on_release(Self::release).detach();
anyhow::Ok(cx.new_model(|model, cx| {
model
.subscribe(&buffer, cx, Self::on_buffer_update)
.detach();
model.on_release(cx, Self::release).detach();
let mut this = Self {
buffer,
buffer_epoch: response.epoch,
@@ -81,11 +85,11 @@ impl ChannelBuffer {
collaborators: Default::default(),
acknowledge_task: None,
channel_id: channel.id,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
subscription: Some(subscription.set_model(model, &mut cx.to_async())),
user_store,
channel_store,
};
this.replace_collaborators(response.collaborators, cx);
this.replace_collaborators(response.collaborators, model, cx);
this
})?)
}
@@ -114,7 +118,8 @@ impl ChannelBuffer {
pub(crate) fn replace_collaborators(
&mut self,
collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let mut new_collaborators = HashMap::default();
for collaborator in collaborators {
@@ -125,14 +130,14 @@ impl ChannelBuffer {
for (_, old_collaborator) in &self.collaborators {
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id, cx)
self.buffer.update(cx, |buffer, model, cx| {
buffer.remove_peer(old_collaborator.replica_id, model, cx)
});
}
}
self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
model.notify(cx);
}
async fn handle_update_channel_buffer(
@@ -147,10 +152,10 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.buffer
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
.update(cx, |buffer, model, cx| buffer.apply_ops(ops, model, cx))
})?;
Ok(())
@@ -161,10 +166,10 @@ impl ChannelBuffer {
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
this.update(&mut cx, |this, model, cx| {
this.replace_collaborators(message.payload.collaborators, model, cx);
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
model.notify(cx);
})
}
@@ -172,7 +177,8 @@ impl ChannelBuffer {
&mut self,
_: Model<language::Buffer>,
event: &language::BufferEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
language::BufferEvent::Operation {
@@ -195,20 +201,20 @@ impl ChannelBuffer {
.log_err();
}
language::BufferEvent::Edited => {
cx.emit(ChannelBufferEvent::BufferEdited);
model.emit(ChannelBufferEvent::BufferEdited, cx);
}
_ => {}
}
}
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
pub fn acknowledge_buffer_version(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let buffer = self.buffer.read(cx);
let version = buffer.version();
let buffer_id = buffer.remote_id().into();
let client = self.client.clone();
let epoch = self.epoch();
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
self.acknowledge_task = Some(cx.spawn(move |cx| async move {
cx.background_executor()
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
.await;
@@ -242,19 +248,19 @@ impl ChannelBuffer {
.cloned()
}
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
pub(crate) fn disconnect(&mut self, model: &Model<Self>, cx: &mut AppContext) {
log::info!("channel buffer {} disconnected", self.channel_id);
if self.connected {
self.connected = false;
self.subscription.take();
cx.emit(ChannelBufferEvent::Disconnected);
cx.notify()
model.emit(ChannelBufferEvent::Disconnected, cx);
model.notify(cx)
}
}
pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(ChannelBufferEvent::ChannelChanged);
cx.notify()
pub(crate) fn channel_changed(&mut self, model: &Model<Self>, cx: &mut AppContext) {
model.emit(ChannelBufferEvent::ChannelChanged, cx);
model.notify(cx)
}
pub fn is_connected(&self) -> bool {

View File

@@ -7,9 +7,7 @@ use client::{
};
use collections::HashSet;
use futures::lock::Mutex;
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
use rand::prelude::*;
use rpc::AnyProtoClient;
use std::{
@@ -119,8 +117,8 @@ impl ChannelChat {
})
.await?;
let handle = cx.new_model(|cx| {
cx.on_release(Self::release).detach();
let handle = cx.new_model(|model, cx| {
model.on_release(cx, Self::release).detach();
Self {
channel_id: channel.id,
user_store: user_store.clone(),
@@ -134,7 +132,7 @@ impl ChannelChat {
last_acknowledged_id: None,
rng: StdRng::from_entropy(),
first_loaded_message_id: None,
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
_subscription: subscription.set_model(model, &mut cx.to_async()),
}
})?;
Self::handle_loaded_messages(
@@ -171,7 +169,8 @@ impl ChannelChat {
pub fn send_message(
&mut self,
message: MessageParams,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<Task<Result<u64>>> {
if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?;
@@ -200,6 +199,7 @@ impl ChannelChat {
},
&(),
),
model,
cx,
);
let user_store = self.user_store.clone();
@@ -207,7 +207,7 @@ impl ChannelChat {
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
// todo - handle messages that fail to send (e.g. >1024 chars)
Ok(cx.spawn(move |this, mut cx| async move {
Ok(model.spawn(cx, move |this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
@@ -221,8 +221,8 @@ impl ChannelChat {
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
let id = response.id;
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
if this.first_loaded_message_id.is_none() {
this.first_loaded_message_id = Some(id);
}
@@ -231,15 +231,20 @@ impl ChannelChat {
}))
}
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn remove_message(
&mut self,
id: u64,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let response = self.rpc.request(proto::RemoveChannelMessage {
channel_id: self.channel_id.0,
message_id: id,
});
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
response.await?;
this.update(&mut cx, |this, cx| {
this.message_removed(id, cx);
this.update(&mut cx, |this, model, cx| {
this.message_removed(id, model, cx);
})?;
Ok(())
})
@@ -249,13 +254,15 @@ impl ChannelChat {
&mut self,
id: u64,
message: MessageParams,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<Task<Result<()>>> {
self.message_update(
ChannelMessageId::Saved(id),
message.text.clone(),
message.mentions.clone(),
Some(OffsetDateTime::now_utc()),
model,
cx,
);
@@ -268,13 +275,17 @@ impl ChannelChat {
nonce: Some(nonce.into()),
mentions: mentions_to_proto(&message.mentions),
});
Ok(cx.spawn(move |_, _| async move {
Ok(cx.spawn(move |_| async move {
request.await?;
Ok(())
}))
}
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
pub fn load_more_messages(
&mut self,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Task<Option<()>>> {
if self.loaded_all_messages {
return None;
}
@@ -283,7 +294,7 @@ impl ChannelChat {
let user_store = self.user_store.clone();
let channel_id = self.channel_id;
let before_message_id = self.first_loaded_message_id()?;
Some(cx.spawn(move |this, mut cx| {
Some(model.spawn(cx, move |this, mut cx| {
async move {
let response = rpc
.request(proto::GetChannelMessages {
@@ -329,7 +340,7 @@ impl ChannelChat {
) -> Option<usize> {
loop {
let step = chat
.update(&mut cx, |chat, cx| {
.update(&mut cx, |chat, model, cx| {
if let Some(first_id) = chat.first_loaded_message_id() {
if first_id <= message_id {
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&());
@@ -347,7 +358,7 @@ impl ChannelChat {
);
}
}
ControlFlow::Continue(chat.load_more_messages(cx))
ControlFlow::Continue(chat.load_more_messages(model, cx))
})
.log_err()?;
match step {
@@ -357,7 +368,7 @@ impl ChannelChat {
}
}
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
pub fn acknowledge_last_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
if self
.last_acknowledged_id
@@ -370,8 +381,8 @@ impl ChannelChat {
})
.ok();
self.last_acknowledged_id = Some(latest_message_id);
self.channel_store.update(cx, |store, cx| {
store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
self.channel_store.update(cx, |store, model, cx| {
store.acknowledge_message_id(self.channel_id, latest_message_id, model, cx);
});
}
}
@@ -388,7 +399,7 @@ impl ChannelChat {
let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
let loaded_message_ids = this.update(cx, |this, _| {
let loaded_message_ids = this.update(cx, |this, model, _| {
let mut loaded_message_ids: HashSet<u64> = HashSet::default();
for message in loaded_messages.iter() {
if let Some(saved_message_id) = message.id.into() {
@@ -425,68 +436,69 @@ impl ChannelChat {
.await?;
Some(messages_from_proto(response.messages, &user_store, cx).await?)
};
this.update(cx, |this, cx| {
this.update(cx, |this, model, cx| {
this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
this.loaded_all_messages = loaded_all_messages;
this.insert_messages(loaded_messages, cx);
this.insert_messages(loaded_messages, model, cx);
if let Some(loaded_ancestors) = loaded_ancestors {
this.insert_messages(loaded_ancestors, cx);
this.insert_messages(loaded_ancestors, model, cx);
}
})?;
Ok(())
}
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
pub fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.channel_id;
cx.spawn(move |this, mut cx| {
async move {
let response = rpc
.request(proto::JoinChannelChat {
channel_id: channel_id.0,
})
.await?;
Self::handle_loaded_messages(
this.clone(),
user_store.clone(),
rpc.clone(),
response.messages,
response.done,
&mut cx,
)
.await?;
let pending_messages = this.update(&mut cx, |this, _| {
this.pending_messages().cloned().collect::<Vec<_>>()
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
body: pending_message.body,
mentions: mentions_to_proto(&pending_message.mentions),
nonce: Some(pending_message.nonce.into()),
reply_to_message_id: pending_message.reply_to_message_id,
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
model
.spawn(cx, move |this, mut cx| {
async move {
let response = rpc
.request(proto::JoinChannelChat {
channel_id: channel_id.0,
})
.await?;
Self::handle_loaded_messages(
this.clone(),
user_store.clone(),
rpc.clone(),
response.messages,
response.done,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
})?;
}
anyhow::Ok(())
}
.log_err()
})
.detach();
let pending_messages = this.update(&mut cx, |this, _, _| {
this.pending_messages().cloned().collect::<Vec<_>>()
})?;
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id: channel_id.0,
body: pending_message.body,
mentions: mentions_to_proto(&pending_message.mentions),
nonce: Some(pending_message.nonce.into()),
reply_to_message_id: pending_message.reply_to_message_id,
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
&mut cx,
)
.await?;
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
})?;
}
anyhow::Ok(())
}
.log_err()
})
.detach();
}
pub fn message_count(&self) -> usize {
@@ -531,7 +543,7 @@ impl ChannelChat {
message: TypedEnvelope<proto::ChannelMessageSent>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let message = message
.payload
.message
@@ -539,12 +551,15 @@ impl ChannelChat {
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
cx.emit(ChannelChatEvent::NewMessage {
channel_id: this.channel_id,
message_id,
})
this.update(&mut cx, |this, model, cx| {
this.insert_messages(SumTree::from_item(message, &()), model, cx);
model.emit(
ChannelChatEvent::NewMessage {
channel_id: this.channel_id,
message_id,
},
cx,
)
})?;
Ok(())
@@ -555,8 +570,8 @@ impl ChannelChat {
message: TypedEnvelope<proto::RemoveChannelMessage>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.message_removed(message.payload.message_id, cx)
this.update(&mut cx, |this, model, cx| {
this.message_removed(message.payload.message_id, model, cx)
})?;
Ok(())
}
@@ -566,7 +581,7 @@ impl ChannelChat {
message: TypedEnvelope<proto::ChannelMessageUpdate>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
let message = message
.payload
.message
@@ -574,19 +589,25 @@ impl ChannelChat {
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.message_update(
message.id,
message.body,
message.mentions,
message.edited_at,
model,
cx,
)
})?;
Ok(())
}
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
fn insert_messages(
&mut self,
messages: SumTree<ChannelMessage>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
let nonces = messages
.cursor::<()>(&())
@@ -631,21 +652,27 @@ impl ChannelChat {
self.messages = new_messages;
for range in ranges.into_iter().rev() {
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: range,
new_count: 0,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: range,
new_count: 0,
},
cx,
);
}
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
},
cx,
);
cx.notify();
model.notify(cx);
}
}
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
fn message_removed(&mut self, id: u64, model: &Model<Self>, cx: &mut AppContext) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
if let Some(item) = cursor.item() {
@@ -658,7 +685,7 @@ impl ChannelChat {
// If the message that was deleted was the last acknowledged message,
// replace the acknowledged message with an earlier one.
self.channel_store.update(cx, |store, _| {
self.channel_store.update(cx, |store, model, _| {
let summary = self.messages.summary();
if summary.count == 0 {
store.set_acknowledged_message_id(self.channel_id, None);
@@ -669,10 +696,13 @@ impl ChannelChat {
}
});
cx.emit(ChannelChatEvent::MessagesUpdated {
old_range: deleted_message_ix..deleted_message_ix + 1,
new_count: 0,
});
model.emit(
ChannelChatEvent::MessagesUpdated {
old_range: deleted_message_ix..deleted_message_ix + 1,
new_count: 0,
},
cx,
);
}
}
}
@@ -683,7 +713,8 @@ impl ChannelChat {
body: String,
mentions: Vec<(Range<usize>, u64)>,
edited_at: Option<OffsetDateTime>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
let mut messages = cursor.slice(&id, Bias::Left, &());
@@ -701,12 +732,15 @@ impl ChannelChat {
drop(cursor);
self.messages = messages;
cx.emit(ChannelChatEvent::UpdateMessage {
message_ix: ix,
message_id: id,
});
model.emit(
ChannelChatEvent::UpdateMessage {
message_ix: ix,
message_id: id,
},
cx,
);
cx.notify();
model.notify(cx);
}
}
@@ -728,8 +762,8 @@ impl ChannelMessage {
cx: &mut AsyncAppContext,
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.get_user(message.sender_id, cx)
.update(cx, |user_store, model, cx| {
user_store.get_user(message.sender_id, model, cx)
})?
.await?;
@@ -779,8 +813,8 @@ impl ChannelMessage {
.into_iter()
.collect();
user_store
.update(cx, |user_store, cx| {
user_store.get_users(unique_user_ids, cx)
.update(cx, |user_store, model, cx| {
user_store.get_users(unique_user_ids, model, cx)
})?
.await?;

View File

@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, User
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, SharedString, Task,
WeakModel,
};
use language::Capability;
use rpc::{
@@ -23,7 +23,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.new_model(|model, cx| ChannelStore::new(client.clone(), user_store.clone(), model, cx));
cx.set_global(GlobalChannelStore(channel_store));
}
@@ -160,32 +160,37 @@ impl ChannelStore {
pub fn new(
client: Arc<Client>,
user_store: Model<UserStore>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let rpc_subscriptions = [
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
client.add_message_handler(model.downgrade(), Self::handle_update_channels),
client.add_message_handler(model.downgrade(), Self::handle_update_user_channels),
];
let mut connection_status = client.status();
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
let watch_connection_status = cx.spawn(|this, mut cx| async move {
let watch_connection_status = model.spawn(cx, |this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade()?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
this.update(&mut cx, |this, model, cx| this.handle_connect(model, cx))
.ok()?
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx))
.ok();
this.update(&mut cx, |this, model, cx| {
this.handle_disconnect(false, model, cx)
})
.ok();
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx))
.ok();
this.update(&mut cx, |this, model, cx| {
this.handle_disconnect(true, model, cx)
})
.ok();
}
}
}
@@ -205,12 +210,12 @@ impl ChannelStore {
_rpc_subscriptions: rpc_subscriptions,
_watch_connection_status: watch_connection_status,
disconnect_channel_buffers_task: None,
_update_channels: cx.spawn(|this, mut cx| async move {
_update_channels: model.spawn(cx, |this, mut cx| async move {
maybe!(async move {
while let Some(update_channels) = update_channels_rx.next().await {
if let Some(this) = this.upgrade() {
let update_task = this.update(&mut cx, |this, cx| {
this.update_channels(update_channels, cx)
let update_task = this.update(&mut cx, |this, model, cx| {
this.update_channels(update_channels, model, cx)
})?;
if let Some(update_task) = update_task {
update_task.await.log_err();
@@ -307,15 +312,17 @@ impl ChannelStore {
pub fn open_channel_buffer(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<ChannelBuffer>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let channel_store = cx.handle();
let channel_store = model.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_buffers,
|channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx),
model,
cx,
)
}
@@ -323,7 +330,8 @@ impl ChannelStore {
pub fn fetch_channel_messages(
&self,
message_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<ChannelMessage>>> {
let request = if message_ids.is_empty() {
None
@@ -333,13 +341,13 @@ impl ChannelStore {
.request(proto::GetChannelMessagesById { message_ids }),
)
};
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if let Some(request) = request {
let response = request.await?;
let this = this
.upgrade()
.ok_or_else(|| anyhow!("channel store dropped"))?;
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
} else {
Ok(Vec::new())
@@ -384,26 +392,28 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.acknowledge_message_id(message_id);
cx.notify();
model.notify(cx);
}
pub fn update_latest_message_id(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.update_latest_message_id(message_id);
cx.notify();
model.notify(cx);
}
pub fn acknowledge_notes_version(
@@ -411,13 +421,14 @@ impl ChannelStore {
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.acknowledge_notes_version(epoch, version);
cx.notify()
model.notify(cx)
}
pub fn update_latest_notes_version(
@@ -425,27 +436,30 @@ impl ChannelStore {
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_states
.entry(channel_id)
.or_default()
.update_latest_notes_version(epoch, version);
cx.notify()
model.notify(cx)
}
pub fn open_channel_chat(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<ChannelChat>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let this = cx.handle();
let this = model.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_chats,
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
model,
cx,
)
}
@@ -460,7 +474,8 @@ impl ChannelStore {
channel_id: ChannelId,
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
load: F,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Model<T>>>
where
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
@@ -483,9 +498,9 @@ impl ChannelStore {
}
},
hash_map::Entry::Vacant(e) => {
let task = cx
.spawn(move |this, mut cx| async move {
let channel = this.update(&mut cx, |this, _| {
let task = model
.spawn(cx, move |this, mut cx| async move {
let channel = this.update(&mut cx, |this, _, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {}", channel_id))
})
@@ -496,25 +511,26 @@ impl ChannelStore {
.shared();
e.insert(OpenedModelHandle::Loading(task.clone()));
cx.spawn({
let task = task.clone();
move |this, mut cx| async move {
let result = task.await;
this.update(&mut cx, |this, _| match result {
Ok(model) => {
get_map(this).insert(
channel_id,
OpenedModelHandle::Open(model.downgrade()),
);
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})
.ok();
}
})
.detach();
model
.spawn(cx, {
let task = task.clone();
move |this, mut cx| async move {
let result = task.await;
this.update(&mut cx, |this, _, _| match result {
Ok(model) => {
get_map(this).insert(
channel_id,
OpenedModelHandle::Open(model.downgrade()),
);
}
Err(_) => {
get_map(this).remove(&channel_id);
}
})
.ok();
}
})
.detach();
break task;
}
}
@@ -572,11 +588,12 @@ impl ChannelStore {
&self,
name: &str,
parent_id: Option<ChannelId>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<ChannelId>> {
let client = self.client.clone();
let name = name.trim_start_matches('#').to_owned();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let response = client
.request(proto::CreateChannel {
name,
@@ -589,12 +606,13 @@ impl ChannelStore {
.ok_or_else(|| anyhow!("missing channel in response"))?;
let channel_id = ChannelId(channel.id);
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let task = this.update_channels(
proto::UpdateChannels {
channels: vec![channel],
..Default::default()
},
model,
cx,
);
assert!(task.is_none());
@@ -603,7 +621,7 @@ impl ChannelStore {
// before this frame is rendered. But we can't guarantee that the collab panel's future
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame completes
cx.emit(ChannelEvent::ChannelCreated(channel_id));
model.emit(ChannelEvent::ChannelCreated(channel_id), cx);
})?;
Ok(channel_id)
@@ -614,10 +632,11 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
to: ChannelId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
let _ = client
.request(proto::MoveChannel {
channel_id: channel_id.0,
@@ -633,10 +652,11 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
visibility: ChannelVisibility,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
let _ = client
.request(proto::SetChannelVisibility {
channel_id: channel_id.0,
@@ -653,15 +673,16 @@ impl ChannelStore {
channel_id: ChannelId,
user_id: UserId,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("invite request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::InviteChannelMember {
channel_id: channel_id.0,
@@ -670,9 +691,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
@@ -685,15 +706,16 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
user_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("invite request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::RemoveChannelMember {
channel_id: channel_id.0,
@@ -701,9 +723,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
Ok(())
@@ -715,15 +737,16 @@ impl ChannelStore {
channel_id: ChannelId,
user_id: UserId,
role: proto::ChannelRole,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
if !self.outgoing_invites.insert((channel_id, user_id)) {
return Task::ready(Err(anyhow!("member request already in progress")));
}
cx.notify();
model.notify(cx);
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let result = client
.request(proto::SetChannelMemberRole {
channel_id: channel_id.0,
@@ -732,9 +755,9 @@ impl ChannelStore {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.outgoing_invites.remove(&(channel_id, user_id));
cx.notify();
model.notify(cx);
})?;
result?;
@@ -746,11 +769,12 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
new_name: &str,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
let name = new_name.to_string();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let channel = client
.request(proto::RenameChannel {
channel_id: channel_id.0,
@@ -759,12 +783,13 @@ impl ChannelStore {
.await?
.channel
.ok_or_else(|| anyhow!("missing channel in response"))?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let task = this.update_channels(
proto::UpdateChannels {
channels: vec![channel],
..Default::default()
},
model,
cx,
);
assert!(task.is_none());
@@ -773,7 +798,7 @@ impl ChannelStore {
// before this frame is rendered. But we can't guarantee that the collab panel's future
// will resolve before this flush_effects finishes. Synchronously emitting this event
// ensures that the collab panel will observe this creation before the frame complete
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
model.emit(ChannelEvent::ChannelRenamed(channel_id), cx)
})?;
Ok(())
})
@@ -783,7 +808,8 @@ impl ChannelStore {
&mut self,
channel_id: ChannelId,
accept: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
@@ -801,11 +827,12 @@ impl ChannelStore {
channel_id: ChannelId,
query: String,
limit: u16,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
let user_store = self.user_store.downgrade();
cx.spawn(move |_, mut cx| async move {
cx.spawn(move |mut cx| async move {
let response = client
.request(proto::GetChannelMembers {
channel_id: channel_id.0,
@@ -813,7 +840,7 @@ impl ChannelStore {
limit: limit as u64,
})
.await?;
user_store.update(&mut cx, |user_store, _| {
user_store.update(&mut cx, |user_store, model, _| {
user_store.insert(response.users);
response
.members
@@ -855,7 +882,7 @@ impl ChannelStore {
message: TypedEnvelope<proto::UpdateChannels>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.update_channels_tx
.unbounded_send(message.payload)
.unwrap();
@@ -868,13 +895,14 @@ impl ChannelStore {
message: TypedEnvelope<proto::UpdateUserChannels>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for buffer_version in message.payload.observed_channel_buffer_version {
let version = language::proto::deserialize_version(&buffer_version.version);
this.acknowledge_notes_version(
ChannelId(buffer_version.channel_id),
buffer_version.epoch,
&version,
model,
cx,
);
}
@@ -882,6 +910,7 @@ impl ChannelStore {
this.acknowledge_message_id(
ChannelId(message_id.channel_id),
message_id.message_id,
model,
cx,
);
}
@@ -896,7 +925,7 @@ impl ChannelStore {
})
}
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
fn handle_connect(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@@ -907,8 +936,8 @@ impl ChannelStore {
for chat in self.opened_chats.values() {
if let OpenedModelHandle::Open(chat) = chat {
if let Some(chat) = chat.upgrade() {
chat.update(cx, |chat, cx| {
chat.rejoin(cx);
chat.update(cx, |chat, model, cx| {
chat.rejoin(model, cx);
});
}
}
@@ -937,17 +966,17 @@ impl ChannelStore {
buffers: buffer_versions,
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let mut response = response.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.opened_buffers.retain(|_, buffer| match buffer {
OpenedModelHandle::Open(channel_buffer) => {
let Some(channel_buffer) = channel_buffer.upgrade() else {
return false;
};
channel_buffer.update(cx, |channel_buffer, cx| {
channel_buffer.update(cx, |channel_buffer, model, cx| {
let channel_id = channel_buffer.channel_id;
if let Some(remote_buffer) = response
.buffers
@@ -960,12 +989,13 @@ impl ChannelStore {
channel_buffer.replace_collaborators(
mem::take(&mut remote_buffer.collaborators),
model,
cx,
);
let operations = channel_buffer
.buffer()
.update(cx, |buffer, cx| {
.update(cx, |buffer, model, cx| {
let outgoing_operations =
buffer.serialize_ops(Some(remote_version), cx);
let incoming_operations =
@@ -973,7 +1003,7 @@ impl ChannelStore {
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>>>()?;
buffer.apply_ops(incoming_operations, cx);
buffer.apply_ops(incoming_operations, model, cx);
anyhow::Ok(outgoing_operations)
})
.log_err();
@@ -999,7 +1029,7 @@ impl ChannelStore {
}
}
channel_buffer.disconnect(cx);
channel_buffer.disconnect(model, cx);
false
})
}
@@ -1011,21 +1041,28 @@ impl ChannelStore {
})
}
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
cx.notify();
fn handle_disconnect(
&mut self,
wait_for_reconnect: bool,
model: &Model<Self>,
cx: &mut AppContext,
) {
model.notify(cx);
self.did_subscribe = false;
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
if wait_for_reconnect {
cx.background_executor().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for (_, buffer) in this.opened_buffers.drain() {
if let OpenedModelHandle::Open(buffer) = buffer {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
buffer.update(cx, |buffer, model, cx| {
buffer.disconnect(model, cx)
});
}
}
}
@@ -1039,7 +1076,8 @@ impl ChannelStore {
pub(crate) fn update_channels(
&mut self,
payload: proto::UpdateChannels,
cx: &mut ModelContext<ChannelStore>,
model: &Model<ChannelStore>,
cx: &mut AppContext,
) -> Option<Task<Result<()>>> {
if !payload.remove_channel_invitations.is_empty() {
self.channel_invitations
@@ -1127,7 +1165,7 @@ impl ChannelStore {
}
}
cx.notify();
model.notify(cx);
if payload.channel_participants.is_empty() {
return None;
}
@@ -1142,13 +1180,13 @@ impl ChannelStore {
}
}
let users = self
.user_store
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
Some(cx.spawn(|this, mut cx| async move {
let users = self.user_store.update(cx, |user_store, model, cx| {
user_store.get_users(all_user_ids, model, cx)
});
Some(model.spawn(cx, |this, mut cx| async move {
let users = users.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
for entry in &channel_participants {
let mut participants: Vec<_> = entry
.participant_user_ids
@@ -1167,7 +1205,7 @@ impl ChannelStore {
.insert(ChannelId(entry.channel_id), participants);
}
cx.notify();
model.notify(cx);
})
}))
}

View File

@@ -137,7 +137,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
let user_id = 5;
let channel_id = 5;
let channel_store = cx.update(init_test);
let client = channel_store.update(cx, |s, _| s.client());
let client = channel_store.update(cx, |s, model, _| s.client());
let server = FakeServer::for_client(user_id, &client, cx).await;
// Get the available channels.
@@ -169,9 +169,9 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
// Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| {
let channel = channel_store.update(cx, |store, model, cx| {
let channel_id = store.ordered_channels().next().unwrap().1.id;
store.open_channel_chat(channel_id, cx)
store.open_channel_chat(channel_id, model, cx)
});
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
server.respond(
@@ -221,7 +221,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
let channel = channel.await.unwrap();
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@@ -270,7 +270,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
new_count: 1,
}
);
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(2..3)
@@ -281,8 +281,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
});
// Scroll up to view older messages.
channel.update(cx, |channel, cx| {
channel.load_more_messages(cx).unwrap().detach();
channel.update(cx, |channel, model, cx| {
channel.load_more_messages(model, cx).unwrap().detach();
});
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
assert_eq!(get_messages.payload.channel_id, 5);
@@ -323,7 +323,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
new_count: 2,
}
);
channel.update(cx, |channel, _| {
channel.update(cx, |channel, model, _| {
assert_eq!(
channel
.messages_in_range(0..2)
@@ -346,7 +346,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let user_store = cx.new_model(|model, cx| UserStore::new(client.clone(), model, cx));
client::init(&client, cx);
crate::init(&client, user_store, cx);
@@ -359,7 +359,9 @@ fn update_channels(
message: proto::UpdateChannels,
cx: &mut AppContext,
) {
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
let task = channel_store.update(cx, |store, model, cx| {
store.update_channels(message, model, cx)
});
assert!(task.is_none());
}
@@ -369,7 +371,7 @@ fn assert_channels(
expected_channels: &[(usize, String)],
cx: &mut AppContext,
) {
let actual = channel_store.update(cx, |store, _| {
let actual = channel_store.update(cx, |store, model, _| {
store
.ordered_channels()
.map(|(depth, channel)| (depth, channel.name.to_string()))

View File

@@ -1962,7 +1962,7 @@ mod tests {
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
AnyProtoClient::from(client.clone()).add_model_message_handler(
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
match model.update(&mut cx, |model, _| model.id).unwrap() {
match model.update(&mut cx, |model, _, _| model.id).unwrap() {
1 => done_tx1.try_send(()).unwrap(),
2 => done_tx2.try_send(()).unwrap(),
_ => unreachable!(),
@@ -1970,15 +1970,15 @@ mod tests {
async { Ok(()) }
},
);
let model1 = cx.new_model(|_| TestModel {
let model1 = cx.new_model(|_, _| TestModel {
id: 1,
subscription: None,
});
let model2 = cx.new_model(|_| TestModel {
let model2 = cx.new_model(|_, _| TestModel {
id: 2,
subscription: None,
});
let model3 = cx.new_model(|_| TestModel {
let model3 = cx.new_model(|_, _| TestModel {
id: 3,
subscription: None,
});
@@ -2018,7 +2018,7 @@ mod tests {
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
let model = cx.new_model(|_, _| TestModel::default());
let (done_tx1, _done_rx1) = smol::channel::unbounded();
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
let subscription1 = client.add_message_handler(
@@ -2053,20 +2053,20 @@ mod tests {
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
let model = cx.new_model(|_, _| TestModel::default());
let (done_tx, mut done_rx) = smol::channel::unbounded();
let subscription = client.add_message_handler(
model.clone().downgrade(),
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
model
.update(&mut cx, |model, _| model.subscription.take())
.update(&mut cx, |this, model, _| this.subscription.take())
.unwrap();
done_tx.try_send(()).unwrap();
async { Ok(()) }
},
);
model.update(cx, |model, _| {
model.subscription = Some(subscription);
model.update(cx, |this, model, _| {
this.subscription = Some(subscription);
});
server.send(proto::Ping {});
done_rx.next().await.unwrap();

View File

@@ -204,7 +204,7 @@ impl FakeServer {
client: Arc<Client>,
cx: &mut TestAppContext,
) -> Model<UserStore> {
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
let user_store = cx.new_model(|model, cx| UserStore::new(client, model, cx));
assert_eq!(
self.receive::<proto::GetUsers>()
.await

View File

@@ -5,8 +5,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt};
use gpui::{
AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, SharedUri, Task,
WeakModel,
AppContext, AsyncAppContext, EventEmitter, Model, SharedString, SharedUri, Task, WeakModel,
};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
@@ -136,14 +135,14 @@ enum UpdateContacts {
}
impl UserStore {
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> Self {
pub fn new(client: Arc<Client>, model: &Model<Self>, cx: &AppContext) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.weak_model(), Self::handle_update_plan),
client.add_message_handler(cx.weak_model(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_model(), Self::handle_show_contacts),
client.add_message_handler(model.downgrade(), Self::handle_update_plan),
client.add_message_handler(model.downgrade(), Self::handle_update_contacts),
client.add_message_handler(model.downgrade(), Self::handle_update_invite_info),
client.add_message_handler(model.downgrade(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
@@ -158,19 +157,19 @@ impl UserStore {
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
_maintain_contacts: cx.spawn(|this, mut cx| async move {
_maintain_contacts: model.spawn(cx, |this, mut cx| async move {
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Ok(task) =
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
{
if let Ok(task) = this.update(&mut cx, |this, model, cx| {
this.update_contacts(message, model, cx)
}) {
task.log_err().await;
} else {
break;
}
}
}),
_maintain_current_user: cx.spawn(|this, mut cx| async move {
_maintain_current_user: model.spawn(cx, |this, mut cx| async move {
let mut status = client.status();
let weak = Arc::downgrade(&client);
drop(client);
@@ -183,8 +182,8 @@ impl UserStore {
Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) = this
.update(&mut cx, |this, cx| {
this.get_user(user_id, cx).log_err()
.update(&mut cx, |this, model, cx| {
this.get_user(user_id, model, cx).log_err()
}) {
fetch_user
} else {
@@ -206,7 +205,7 @@ impl UserStore {
staff,
);
this.update(cx, |this, _| {
this.update(cx, |this, model, _| {
this.set_current_user_accepted_tos_at(
info.accepted_tos_at,
);
@@ -218,20 +217,20 @@ impl UserStore {
current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| cx.notify())?;
this.update(&mut cx, |_, model, cx| model.notify(cx))?;
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.clear_contacts()
})?
.await;
}
Status::ConnectionLost => {
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
this.clear_contacts()
})?
.await;
@@ -242,7 +241,7 @@ impl UserStore {
Ok(())
}),
pending_contact_requests: Default::default(),
weak_self: cx.weak_model(),
weak_self: model.downgrade(),
}
}
@@ -257,12 +256,12 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateInviteInfo>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.invite_info = Some(InviteInfo {
url: Arc::from(message.payload.url),
count: message.payload.count,
});
cx.notify();
model.notify(cx);
})?;
Ok(())
}
@@ -272,7 +271,7 @@ impl UserStore {
_: TypedEnvelope<proto::ShowContacts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
this.update(&mut cx, |_, model, cx| model.emit(Event::ShowContacts, cx))?;
Ok(())
}
@@ -285,7 +284,7 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateContacts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
@@ -298,9 +297,9 @@ impl UserStore {
message: TypedEnvelope<proto::UpdateUserPlan>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.current_plan = Some(message.payload.plan());
cx.notify();
model.notify(cx);
})?;
Ok(())
}
@@ -308,7 +307,8 @@ impl UserStore {
fn update_contacts(
&mut self,
message: UpdateContacts,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
@@ -330,8 +330,8 @@ impl UserStore {
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
cx.spawn(|this, mut cx| async move {
let load_users = self.get_users(user_ids.into_iter().collect(), model, cx);
model.spawn(cx, |this, mut cx| async move {
load_users.await?;
// Users are fetched in parallel above and cached in call to get_users
@@ -349,8 +349,8 @@ impl UserStore {
let mut incoming_requests = Vec::new();
for request in message.incoming_requests {
incoming_requests.push({
this.update(&mut cx, |this, cx| {
this.get_user(request.requester_id, cx)
this.update(&mut cx, |this, model, cx| {
this.get_user(request.requester_id, model, cx)
})?
.await?
});
@@ -359,8 +359,10 @@ impl UserStore {
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
.await?,
this.update(&mut cx, |this, model, cx| {
this.get_user(requested_user_id, model, cx)
})?
.await?,
);
}
@@ -371,7 +373,7 @@ impl UserStore {
let removed_outgoing_requests =
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
// Remove contacts
this.contacts
.retain(|contact| !removed_contacts.contains(&contact.user.id));
@@ -389,10 +391,13 @@ impl UserStore {
// Remove incoming contact requests
this.incoming_contact_requests.retain(|user| {
if removed_incoming_requests.contains(&user.id) {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
});
model.emit(
Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
},
cx,
);
false
} else {
true
@@ -425,7 +430,7 @@ impl UserStore {
}
}
cx.notify();
model.notify(cx);
})?;
Ok(())
@@ -483,17 +488,24 @@ impl UserStore {
pub fn request_contact(
&mut self,
responder_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
self.perform_contact_request(
responder_id,
proto::RequestContact { responder_id },
model,
cx,
)
}
pub fn remove_contact(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, model, cx)
}
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
@@ -506,7 +518,8 @@ impl UserStore {
&mut self,
requester_id: u64,
accept: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.perform_contact_request(
requester_id,
@@ -518,6 +531,7 @@ impl UserStore {
proto::ContactRequestResponse::Decline
} as i32,
},
model,
cx,
)
}
@@ -525,10 +539,11 @@ impl UserStore {
pub fn dismiss_contact_request(
&self,
requester_id: u64,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn(move |_, _| async move {
cx.spawn(move |_| async move {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RespondToContactRequest {
@@ -544,18 +559,19 @@ impl UserStore {
&mut self,
user_id: u64,
request: T,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
cx.notify();
model.notify(cx);
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(request)
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(user_id)
{
@@ -564,7 +580,7 @@ impl UserStore {
request_count.remove();
}
}
cx.notify();
model.notify(cx);
})?;
response?;
Ok(())
@@ -594,25 +610,27 @@ impl UserStore {
pub fn get_users(
&self,
user_ids: Vec<u64>,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
model,
cx,
)
})?
.await?;
}
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
user_ids
.iter()
.map(|user_id| {
@@ -629,33 +647,44 @@ impl UserStore {
pub fn fuzzy_search_users(
&self,
query: String,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
self.load_users(proto::FuzzySearchUsers { query }, cx)
self.load_users(proto::FuzzySearchUsers { query }, model, cx)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext<Self>) -> Option<Arc<User>> {
pub fn get_user_optimistic(
&self,
user_id: u64,
model: &Model<Self>,
cx: &AppContext,
) -> Option<Arc<User>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Some(user);
}
self.get_user(user_id, cx).detach_and_log_err(cx);
self.get_user(user_id, model, cx).detach_and_log_err(cx);
None
}
pub fn get_user(&self, user_id: u64, cx: &ModelContext<Self>) -> Task<Result<Arc<User>>> {
pub fn get_user(
&self,
user_id: u64,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Task::ready(Ok(user));
}
let load_users = self.get_users(vec![user_id], cx);
cx.spawn(move |this, mut cx| async move {
let load_users = self.get_users(vec![user_id], model, cx);
model.spawn(cx, move |this, mut cx| async move {
load_users.await?;
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.users
.get(&user_id)
.cloned()
@@ -687,20 +716,24 @@ impl UserStore {
.map(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&self, cx: &ModelContext<Self>) -> Task<Result<()>> {
pub fn accept_terms_of_service(
&self,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
let client = self.client.clone();
cx.spawn(move |this, mut cx| async move {
model.spawn(cx, move |this, mut cx| async move {
if let Some(client) = client.upgrade() {
let response = client
.request(proto::AcceptTermsOfService {})
.await
.context("error accepting tos")?;
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at))
})
} else {
@@ -718,15 +751,16 @@ impl UserStore {
fn load_users(
&self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &ModelContext<Self>,
model: &Model<Self>,
cx: &AppContext,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = response.users;
this.update(&mut cx, |this, _| this.insert(users))
this.update(&mut cx, |this, _, _| this.insert(users))
} else {
Ok(Vec::new())
}
@@ -752,11 +786,12 @@ impl UserStore {
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if participant_indices != self.participant_indices {
self.participant_indices = participant_indices;
cx.emit(Event::ParticipantIndicesChanged);
model.emit(Event::ParticipantIndicesChanged, cx);
}
}
@@ -781,8 +816,10 @@ impl UserStore {
if !missing_user_ids.is_empty() {
let this = self.weak_self.clone();
cx.spawn(|mut cx| async move {
this.update(&mut cx, |this, cx| this.get_users(missing_user_ids, cx))?
.await
this.update(&mut cx, |this, model, cx| {
this.get_users(missing_user_ids, model, cx)
})?
.await
})
.detach_and_log_err(cx);
}
@@ -807,8 +844,8 @@ impl Contact {
cx: &mut AsyncAppContext,
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.get_user(contact.user_id, cx)
.update(cx, |user_store, model, cx| {
user_store.get_user(contact.user_id, model, cx)
})?
.await?;
Ok(Self {

View File

@@ -9,7 +9,7 @@ use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use editor::{Anchor, Editor, ToOffset};
use futures::future;
use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext};
use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext};
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use serde_json::json;
use std::ops::Range;
@@ -161,43 +161,43 @@ async fn test_channel_notes_participant_indices(
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), model, cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), model, cx))
.await
.unwrap();
// Clients A, B, and C all insert and select some text
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.insert("a", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
});
executor.run_until_parked();
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("b", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
});
executor.run_until_parked();
channel_view_c.update(cx_c, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("c", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -207,8 +207,8 @@ async fn test_channel_notes_participant_indices(
// in a call together.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], model, cx);
});
});
@@ -223,19 +223,21 @@ async fn test_channel_notes_participant_indices(
// still doesn't have a color.
executor.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
model,
cx,
);
});
});
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
model,
cx,
);
});
@@ -270,12 +272,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -283,10 +285,10 @@ async fn test_channel_notes_participant_indices(
// Clients A and B see each other with the same colors as in the channel notes.
editor_a.update(cx_a, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], model, cx);
});
editor_b.update(cx_b, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], model, cx);
});
}
@@ -294,9 +296,10 @@ async fn test_channel_notes_participant_indices(
fn assert_remote_selections(
editor: &mut Editor,
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) {
let snapshot = editor.snapshot(cx);
let snapshot = editor.snapshot(model, cx);
let range = Anchor::min()..Anchor::max();
let remote_selections = snapshot
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
@@ -344,7 +347,7 @@ async fn test_multiple_handles_to_channel_buffer(
assert_eq!(channel_buffer, channel_buffer_3);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "hello")], None, cx);
})
});
@@ -365,7 +368,7 @@ async fn test_multiple_handles_to_channel_buffer(
.unwrap();
assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id);
channel_buffer.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, _| {
buffer.buffer().update(cx, |buffer, model, _| {
assert_eq!(buffer.text(), "hello");
})
});
@@ -465,7 +468,7 @@ async fn test_rejoin_channel_buffer(
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
@@ -477,12 +480,12 @@ async fn test_rejoin_channel_buffer(
// Both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
@@ -552,7 +555,7 @@ async fn test_channel_buffers_and_server_restarts(
.unwrap();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
@@ -567,12 +570,12 @@ async fn test_channel_buffers_and_server_restarts(
// While the server is down, both clients make an edit.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(1..1, "2")], None, cx);
})
});
channel_buffer_b.update(cx_b, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "0")], None, cx);
})
});
@@ -642,8 +645,8 @@ async fn test_channel_buffer_changes(
// Closing the buffer should re-enable change tracking
cx_b.update(|cx| {
workspace_b.update(cx, |workspace, cx| {
workspace.close_all_items_and_panes(&Default::default(), cx)
workspace_b.update(cx, |workspace, model, cx| {
workspace.close_all_items_and_panes(&Default::default(), model, cx)
});
});
deterministic.run_until_parked();

View File

@@ -118,7 +118,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
// B is promoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
@@ -146,7 +146,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
// B is demoted
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Guest,
@@ -238,7 +238,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
// yet signed the zed CLA.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,
@@ -258,7 +258,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
// yet signed the zed CLA.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Talker,
@@ -285,7 +285,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
// A can now grant write access to B.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
call.room().unwrap().update(cx, |room, model, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Member,

View File

@@ -82,8 +82,15 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
let workspace_b = cx_b.add_window(|cx| {
Workspace::new(
None,
project_b.clone(),
client_b.app_state.clone(),
model,
cx,
)
});
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();
@@ -98,7 +105,7 @@ async fn test_host_disconnect(
.unwrap();
//TODO: focus
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(window)));
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
cx_b.update(|cx| {
@@ -201,7 +208,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), model, cx));
let mut editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
@@ -216,7 +223,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), model, cx));
let mut editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
@@ -317,8 +324,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
let editor_b =
cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx));
let editor_b = cx_b
.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.background_executor.run_until_parked();
@@ -329,7 +336,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(".", cx);
});
cx_b.focus_view(&editor_b);
@@ -442,7 +449,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([46..46]));
editor.change_selections(None, model, cx, |s| s.select_ranges([46..46]));
editor.handle_input("; a", cx);
editor.handle_input(".", cx);
});
@@ -594,7 +601,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@@ -675,7 +682,7 @@ async fn test_collaborating_with_code_actions(
// Confirming the code action will trigger a resolve request.
let confirm_action = editor_b
.update(cx_b, |editor, cx| {
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, cx)
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, model, cx)
})
.unwrap();
fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
@@ -796,7 +803,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
editor.change_selections(None, model, cx, |s| s.select_ranges([7..7]));
editor.rename(&Rename, cx).unwrap()
});
@@ -821,14 +828,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
6..9
);
rename.editor.update(cx, |rename_editor, cx| {
rename.editor.update(cx, |rename_editor, model, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
0..3,
"Rename that was triggered from zero selection caret, should propose the whole word."
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
rename_buffer.edit([(0..3, "THREE")], None, cx);
});
});
@@ -839,7 +846,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, cx);
});
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([7..8]));
editor.change_selections(None, model, cx, |s| s.select_ranges([7..8]));
editor.rename(&Rename, cx).unwrap()
});
@@ -863,21 +870,21 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
let lsp_rename_start = rename.range.start.to_offset(&buffer);
let lsp_rename_end = rename.range.end.to_offset(&buffer);
assert_eq!(lsp_rename_start..lsp_rename_end, 6..9);
rename.editor.update(cx, |rename_editor, cx| {
rename.editor.update(cx, |rename_editor, model, cx| {
let rename_selection = rename_editor.selections.newest::<usize>(cx);
assert_eq!(
rename_selection.range(),
1..2,
"Rename that was triggered from a selection, should have the same selection range in the rename proposal"
);
rename_editor.buffer().update(cx, |rename_buffer, cx| {
rename_editor.buffer().update(cx, |rename_buffer, model, cx| {
rename_buffer.edit([(0..lsp_rename_end - lsp_rename_start, "THREE")], None, cx);
});
});
});
let confirm_rename = editor_b.update(cx_b, |editor, cx| {
Editor::confirm_rename(editor, &ConfirmRename, cx).unwrap()
Editor::confirm_rename(editor, &ConfirmRename, model, cx).unwrap()
});
fake_language_server
.handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
@@ -1191,7 +1198,7 @@ async fn test_share_project(
.await
.unwrap();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx));
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, model, cx));
// Client A sees client B's selection
executor.run_until_parked();
@@ -1296,7 +1303,8 @@ async fn test_on_input_format_from_host_to_guest(
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx));
let editor_a =
cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
@@ -1330,7 +1338,7 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus_view(&editor_a);
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(">", cx);
});
@@ -1416,7 +1424,8 @@ async fn test_on_input_format_from_guest_to_host(
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx));
let editor_b =
cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), model, cx));
let fake_language_server = fake_language_servers.next().await.unwrap();
executor.run_until_parked();
@@ -1424,7 +1433,7 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus_view(&editor_b);
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input(":", cx);
});
@@ -1668,7 +1677,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13].clone()));
editor.handle_input(":", cx);
});
cx_b.focus_view(&editor_b);
@@ -1693,7 +1702,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.change_selections(None, model, cx, |s| s.select_ranges([13..13]));
editor.handle_input("a change to increment both buffers' versions", cx);
});
cx_a.focus_view(&editor_a);
@@ -2070,7 +2079,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2086,7 +2095,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
]
);
blame.update(cx, |blame, _| {
blame.update(cx, |blame, model, _| {
for (idx, entry) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
@@ -2109,7 +2118,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2136,7 +2145,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
editor_b.update(cx_b, |editor_b, cx| {
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
let entries = blame.update(cx, |blame, model, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.collect::<Vec<_>>()
@@ -2206,9 +2215,9 @@ async fn test_collaborating_with_editorconfig(
.unwrap();
let cx_a = cx_a.add_empty_window();
let main_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), model, cx));
let other_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), model, cx));
let mut main_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
@@ -2238,9 +2247,9 @@ async fn test_collaborating_with_editorconfig(
.unwrap();
let cx_b = cx_b.add_empty_window();
let main_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), model, cx));
let other_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), model, cx));
let mut main_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),

View File

@@ -8,8 +8,8 @@ use collab_ui::{
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
View, VisualContext, VisualTestContext,
point, AppContext, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString,
TestAppContext, View, VisualContext, VisualTestContext,
};
use language::Capability;
use project::WorktreeSettings;
@@ -273,12 +273,12 @@ async fn test_basic_following(
// When client A opens a multibuffer, client B does so as well.
let multibuffer_a = cx_a.new_model(|cx| {
let buffer_a1 = project_a.update(cx, |project, cx| {
let buffer_a1 = project_a.update(cx, |project, model, cx| {
project
.get_open_buffer(&(worktree_id, "1.txt").into(), cx)
.unwrap()
});
let buffer_a2 = project_a.update(cx, |project, cx| {
let buffer_a2 = project_a.update(cx, |project, model, cx| {
project
.get_open_buffer(&(worktree_id, "2.txt").into(), cx)
.unwrap()
@@ -290,6 +290,7 @@ async fn test_basic_following(
context: 0..3,
primary: None,
}],
model,
cx,
);
result.push_excerpts(
@@ -298,15 +299,16 @@ async fn test_basic_following(
context: 4..7,
primary: None,
}],
model,
cx,
);
result
});
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
let editor = cx.new_model(|model, cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, model, cx)
});
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, model, cx);
editor
});
executor.run_until_parked();
@@ -367,7 +369,7 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1, 2..2]));
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -382,7 +384,7 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.change_selections(None, model, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(point(0., 100.), cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -444,7 +446,7 @@ async fn test_basic_following(
.update(cx_b, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, model, cx| room.share_screen(cx))
})
.await
.unwrap(); // This is what breaks
@@ -513,7 +515,7 @@ async fn test_basic_following(
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.new_view(TestItem::new);
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
})
});
@@ -605,8 +607,8 @@ async fn test_following_tab_order(
.await
.unwrap();
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
let pane_paths = |pane: &Model<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, model, cx| {
pane.items()
.map(|item| {
item.project_path(cx)
@@ -924,7 +926,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B focuses a file that they previously followed A to, breaking
// the follow.
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -975,7 +977,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B closes tabs, some of which were originally opened by client A,
// and some of which were originally opened by client B.
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.close_inactive_items(&Default::default(), cx)
.unwrap()
.detach();
@@ -1028,7 +1030,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
executor.run_until_parked();
// Client A cycles through some tabs.
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1072,7 +1074,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1119,7 +1121,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
);
workspace_a.update(cx_a, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
workspace.active_pane().update(cx, |pane, model, cx| {
pane.activate_prev_item(true, cx);
});
});
@@ -1608,7 +1610,7 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([1..1]))
editor.change_selections(None, model, cx, |s| s.select_ranges([1..1]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1618,17 +1620,18 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
});
// a unshares the project
cx_a.update(|cx| {
// todo! why do i need the annotation
cx_a.update(|cx: &mut AppContext| {
let project = workspace_a.read(cx).project().clone();
ActiveCall::global(cx).update(cx, |call, cx| {
call.unshare_project(project, cx).unwrap();
ActiveCall::global(cx).update(cx, |call, model, cx| {
call.unshare_project(project, model, cx).unwrap();
})
});
cx_a.run_until_parked();
// b should not follow a to position 2
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..2]))
editor.change_selections(None, model, cx, |s| s.select_ranges([2..2]))
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1776,11 +1779,11 @@ async fn test_following_into_excluded_file(
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::View<ProjectSharedNotification>> {
) -> Vec<gpui::Model<ProjectSharedNotification>> {
let mut ret = Vec::new();
for window in cx.windows() {
window
.update(cx, |window, _| {
.update(cx, |window, model, _| {
if let Ok(handle) = window.downcast::<ProjectSharedNotification>() {
ret.push(handle)
}
@@ -1821,8 +1824,8 @@ fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec
})
}
fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
workspace.update(cx, |workspace, cx| {
fn pane_summaries(workspace: &Model<Workspace>, cx: &mut VisualTestContext) -> Vec<PaneSummary> {
workspace.update(cx, |workspace, model, cx| {
let active_pane = workspace.active_pane();
workspace
.panes()
@@ -1924,14 +1927,14 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 1.
let channel_notes_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -1963,7 +1966,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
notes.editor.update(cx, |editor, model, cx| {
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
})
@@ -1971,7 +1974,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 2.
let channel_notes_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), model, cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {
@@ -2027,12 +2030,12 @@ pub(crate) async fn join_channel(
}
async fn share_workspace(
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
cx: &mut VisualTestContext,
) -> anyhow::Result<u64> {
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
cx.read(ActiveCall::global)
.update(cx, |call, cx| call.share_project(project, cx))
.update(cx, |call, model, cx| call.share_project(project, model, cx))
.await
}
@@ -2071,7 +2074,7 @@ async fn test_following_to_channel_notes_other_workspace(
let (workspace_a2, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
cx_a2.update(|cx| cx.activate_window());
cx_a2
.update(|cx| ChannelView::open(channel, None, workspace_a2, cx))
.update(|cx| ChannelView::open(channel, None, workspace_a2, model, cx))
.await
.unwrap();
cx_a2.run_until_parked();

View File

@@ -252,7 +252,7 @@ async fn test_basic_calls(
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, model, cx| room.share_screen(cx))
})
.await
.unwrap();
@@ -6078,7 +6078,7 @@ async fn test_join_call_after_screen_was_shared(
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
.update(cx, |room, model, cx| room.share_screen(cx))
})
.await
.unwrap();
@@ -6173,11 +6173,11 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
let (workspace, cx) = client.build_test_workspace(cx).await;
cx.simulate_keystrokes("cmd-n");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 1);
});
cx.simulate_keystrokes("cmd-k left");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
cx.simulate_keystrokes("cmd-k");
@@ -6185,7 +6185,7 @@ async fn test_pane_split_left(cx: &mut TestAppContext) {
// to verify that it doesn't fire in this case.
cx.executor().advance_clock(Duration::from_secs(2));
cx.simulate_keystrokes("left");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
}
@@ -6207,9 +6207,9 @@ async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppCont
async fn test_preview_tabs(cx: &mut TestAppContext) {
let (_server, client) = TestServer::start1(cx).await;
let (workspace, cx) = client.build_test_workspace(cx).await;
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let project = workspace.update(cx, |workspace, model, _| workspace.project().clone());
let worktree_id = project.update(cx, |project, cx| {
let worktree_id = project.update(cx, |project, model, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
@@ -6226,7 +6226,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
path: Path::new("3.rs").into(),
};
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
@@ -6234,13 +6234,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Opening item 3 as a "permanent" tab
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path(path_3.clone(), None, false, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6251,13 +6251,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 1 as preview
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6272,13 +6272,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6293,11 +6293,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6311,7 +6311,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Closing item 1
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
pane.close_item_by_id(
pane.active_item().unwrap().item_id(),
workspace::SaveIntent::Skip,
@@ -6321,7 +6321,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6332,11 +6332,11 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.update(cx, |workspace, model, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
@@ -6350,14 +6350,14 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Close permanent tab
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
let id = pane.items().next().unwrap().item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6370,13 +6370,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Split pane to the right
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
pane.split(workspace::SplitDirection::Right, cx);
});
let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let right_pane = workspace.update(cx, |workspace, model, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6388,7 +6388,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(pane.preview_item_id(), None);
@@ -6399,13 +6399,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Open item 2 as preview in right pane
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
@@ -6417,7 +6417,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6431,19 +6431,19 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Focus left pane
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
});
// Open item 2 as preview in left pane
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_2.clone());
assert_eq!(
@@ -6455,7 +6455,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(!pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
right_pane.update(cx, |pane, model, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
@@ -6545,12 +6545,12 @@ async fn test_context_collaboration_with_reconnect(
// Host and guest make changes
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Host change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Guest change\n")], None, cx)
})
});
@@ -6568,12 +6568,12 @@ async fn test_context_collaboration_with_reconnect(
server.disconnect_client(client_a.peer_id().unwrap());
server.forbid_connections();
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Host offline change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
context.buffer().update(cx, |buffer, model, cx| {
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
})
});
@@ -6643,7 +6643,7 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.update(|cx| project_b.update(cx, |project, model, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
@@ -6657,7 +6657,7 @@ async fn test_remote_git_branches(
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
@@ -6667,8 +6667,8 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
project_a.update(cx, |project, model, cx| {
project.worktree_store().update(cx, |worktree_store, model, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
@@ -6680,7 +6680,7 @@ async fn test_remote_git_branches(
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
@@ -6690,8 +6690,8 @@ async fn test_remote_git_branches(
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
project_a.update(cx, |project, model, cx| {
project.worktree_store().update(cx, |worktree_store, model, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})

View File

@@ -21,14 +21,14 @@ async fn test_notifications(
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
client_a.notification_store().update(cx_a, |_, cx| {
let events = notification_events_a.clone();
cx.subscribe(&cx.handle(), move |_, _, event, _| {
cx.subscribe(model, move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()
});
client_b.notification_store().update(cx_b, |_, cx| {
let events = notification_events_b.clone();
cx.subscribe(&cx.handle(), move |_, _, event, _| {
cx.subscribe(model, move |_, _, event, _| {
events.lock().push(event.clone());
})
.detach()

View File

@@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
) -> Result<(), TestError> {
match operation {
ChannelBufferOperation::JoinChannelNotes { channel_name } => {
let buffer = client.channel_store().update(cx, |store, cx| {
let buffer = client.channel_store().update(cx, |store, model, cx| {
let channel_id = store
.ordered_channels()
.find(|(_, c)| c.name == channel_name)
@@ -198,9 +198,9 @@ impl RandomizedTest for RandomChannelBufferTest {
edits
);
channel_buffer.update(cx, |buffer, cx| {
channel_buffer.update(cx, |buffer, model, cx| {
let buffer = buffer.buffer();
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let snapshot = buffer.snapshot();
buffer.edit(
edits.into_iter().map(|(range, text)| {

View File

@@ -508,7 +508,7 @@ impl RandomizedTest for ProjectCollaborationTest {
log::info!("{}: accepting incoming call", client.username);
active_call
.update(cx, |call, cx| call.accept_incoming(cx))
.update(cx, |call, model, cx| call.accept_incoming(model, cx))
.await?;
}
@@ -519,7 +519,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
log::info!("{}: declining incoming call", client.username);
active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
active_call.update(cx, |call, model, cx| call.decline_incoming(model, cx))?;
}
ClientOperation::LeaveCall => {
@@ -529,7 +529,9 @@ impl RandomizedTest for ProjectCollaborationTest {
}
log::info!("{}: hanging up", client.username);
active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
active_call
.update(cx, |call, model, cx| call.hang_up(model, cx))
.await?;
}
ClientOperation::InviteContactToCall { user_id } => {
@@ -537,7 +539,9 @@ impl RandomizedTest for ProjectCollaborationTest {
log::info!("{}: inviting {}", client.username, user_id,);
active_call
.update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
.update(cx, |call, model, cx| {
call.invite(user_id.to_proto(), None, model, cx)
})
.await
.log_err();
}
@@ -580,8 +584,8 @@ impl RandomizedTest for ProjectCollaborationTest {
client.fs().create_dir(&new_root_path).await.unwrap();
}
project
.update(cx, |project, cx| {
project.find_or_create_worktree(&new_root_path, true, cx)
.update(cx, |project, model, cx| {
project.find_or_create_worktree(&new_root_path, true, model, cx)
})
.await
.unwrap();
@@ -615,7 +619,7 @@ impl RandomizedTest for ProjectCollaborationTest {
} => {
let active_call = cx.read(ActiveCall::global);
let project = active_call
.update(cx, |call, cx| {
.update(cx, |call, model, cx| {
let room = call.room().cloned()?;
let participant = room
.read(cx)
@@ -626,11 +630,12 @@ impl RandomizedTest for ProjectCollaborationTest {
.iter()
.find(|project| project.worktree_root_names[0] == first_root_name)?
.id;
Some(room.update(cx, |room, cx| {
Some(room.update(cx, |room, model, cx| {
room.join_project(
project_id,
client.language_registry().clone(),
FakeFs::new(cx.background_executor().clone()),
model,
cx,
)
}))
@@ -670,7 +675,9 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
.update(cx, |p, model, cx| {
p.create_entry(project_path, is_dir, model, cx)
})
.await?;
}
@@ -694,7 +701,9 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.update(cx, |project, model, cx| {
project.open_buffer(project_path, model, cx)
})
.await?;
client.buffers_for_project(&project).insert(buffer);
}
@@ -720,7 +729,7 @@ impl RandomizedTest for ProjectCollaborationTest {
);
ensure_project_shared(&project, client, cx).await;
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let snapshot = buffer.snapshot();
buffer.edit(
edits.into_iter().map(|(range, text)| {
@@ -729,6 +738,7 @@ impl RandomizedTest for ProjectCollaborationTest {
(start..end, text)
}),
None,
model,
cx,
);
});
@@ -781,8 +791,9 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
let save =
project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
let save = project.update(cx, |project, model, cx| {
project.save_buffer(buffer.clone(), model, cx)
});
let save = cx.spawn(|cx| async move {
save.await
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
@@ -825,25 +836,25 @@ impl RandomizedTest for ProjectCollaborationTest {
use futures::{FutureExt as _, TryFutureExt as _};
let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
let process_lsp_request = project.update(cx, |project, cx| match kind {
let process_lsp_request = project.update(cx, |project, model, cx| match kind {
LspRequestKind::Rename => project
.prepare_rename(buffer, offset, cx)
.prepare_rename(buffer, offset, model, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::Completion => project
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, model, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::CodeAction => project
.code_actions(&buffer, offset..offset, None, cx)
.code_actions(&buffer, offset..offset, None, model, cx)
.map(|_| Ok(()))
.boxed(),
LspRequestKind::Definition => project
.definition(&buffer, offset, cx)
.definition(&buffer, offset, model, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::Highlights => project
.document_highlights(&buffer, offset, cx)
.document_highlights(&buffer, offset, model, cx)
.map_ok(|_| ())
.boxed(),
});
@@ -873,7 +884,7 @@ impl RandomizedTest for ProjectCollaborationTest {
if detach { "detaching" } else { "awaiting" }
);
let mut search = project.update(cx, |project, cx| {
let mut search = project.update(cx, |project, model, cx| {
project.search(
SearchQuery::text(
query,
@@ -885,6 +896,7 @@ impl RandomizedTest for ProjectCollaborationTest {
None,
)
.unwrap(),
model,
cx,
)
});
@@ -1573,7 +1585,9 @@ async fn ensure_project_shared(
&& project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
{
match active_call
.update(cx, |call, cx| call.share_project(project.clone(), cx))
.update(cx, |call, model, cx| {
call.share_project(project.clone(), model, cx)
})
.await
{
Ok(project_id) => {

View File

@@ -73,7 +73,7 @@ async fn test_sharing_an_ssh_remote_project(
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let _headless_project = server_cx.new_model(|cx| {
let _headless_project = server_cx.new_model(|model, cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
@@ -236,7 +236,7 @@ async fn test_ssh_collaboration_git_branches(
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let headless_project = server_cx.new_model(|cx| {
let headless_project = server_cx.new_model(|model, cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
@@ -273,7 +273,7 @@ async fn test_ssh_collaboration_git_branches(
let root_path = ProjectPath::root_path(worktree_id);
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.update(|cx| project_b.update(cx, |project, model, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
@@ -287,7 +287,7 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
@@ -297,10 +297,10 @@ async fn test_ssh_collaboration_git_branches(
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.update(cx, |headless_project, model, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
.update(cx, |worktree_store, model, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
@@ -312,7 +312,7 @@ async fn test_ssh_collaboration_git_branches(
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project_b.update(cx, |project, model, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
@@ -322,10 +322,10 @@ async fn test_ssh_collaboration_git_branches(
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.update(cx, |headless_project, model, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
.update(cx, |worktree_store, model, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
@@ -394,7 +394,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let _headless_project = server_cx.new_model(|cx| {
let _headless_project = server_cx.new_model(|model, cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {

View File

@@ -273,10 +273,11 @@ impl TestServer {
git_hosting_provider_registry
.register_hosting_provider(Arc::new(git_hosting_providers::Github));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
let user_store = cx.new_model(|model, cx| UserStore::new(client.clone(), model, cx));
let workspace_store =
cx.new_model(|model, cx| WorkspaceStore::new(client.clone(), model, cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
let session = cx.new_model(|model, cx| AppSession::new(Session::test(), model, cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
@@ -435,10 +436,11 @@ impl TestServer {
client
.channel_store()
.update(cx, |channel_store, cx| {
.update(cx, |channel_store, model, cx| {
channel_store.set_channel_visibility(
channel_id,
proto::ChannelVisibility::Public,
model,
cx,
)
})
@@ -634,7 +636,7 @@ impl TestClient {
pub async fn clear_contacts(&self, cx: &mut TestAppContext) {
self.app_state
.user_store
.update(cx, |store, _| store.clear_contacts())
.update(cx, |store, model, _| store.clear_contacts())
.await;
}
@@ -705,7 +707,9 @@ impl TestClient {
) -> (Model<Project>, WorktreeId) {
let project = self.build_empty_local_project(cx);
let (worktree, _) = project
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
.update(cx, |p, model, cx| {
p.find_or_create_worktree(root_path, true, model, cx)
})
.await
.unwrap();
worktree
@@ -732,7 +736,9 @@ impl TestClient {
)
});
let (worktree, _) = project
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
.update(cx, |p, model, cx| {
p.find_or_create_worktree(root_path, true, model, cx)
})
.await
.unwrap();
(project, worktree.read_with(cx, |tree, _| tree.id()))
@@ -754,20 +760,22 @@ impl TestClient {
pub async fn host_workspace(
&self,
workspace: &View<Workspace>,
workspace: &Model<Workspace>,
channel_id: ChannelId,
cx: &mut VisualTestContext,
) {
cx.update(|cx| {
let active_call = ActiveCall::global(cx);
active_call.update(cx, |call, cx| call.join_channel(channel_id, cx))
active_call.update(cx, |call, model, cx| {
call.join_channel(channel_id, model, cx)
})
})
.await
.unwrap();
cx.update(|cx| {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call.update(cx, |call, cx| call.share_project(project, cx))
active_call.update(cx, |call, model, cx| call.share_project(project, model, cx))
})
.await
.unwrap();
@@ -778,7 +786,7 @@ impl TestClient {
&'a self,
channel_id: ChannelId,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
.await
.unwrap();
@@ -824,28 +832,42 @@ impl TestClient {
&'a self,
project: &Model<Project>,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
Workspace::new(
None,
project.clone(),
self.app_state.clone(),
model,
window,
cx,
)
})
}
pub async fn build_test_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
let project = self.build_test_project(cx).await;
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
Workspace::new(
None,
project.clone(),
self.app_state.clone(),
model,
window,
cx,
)
})
}
pub fn active_workspace<'a>(
&'a self,
cx: &'a mut TestAppContext,
) -> (View<Workspace>, &'a mut VisualTestContext) {
) -> (Model<Workspace>, &'a mut VisualTestContext) {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let view = window.root_view(cx).unwrap();
@@ -858,11 +880,11 @@ impl TestClient {
pub fn open_channel_notes(
channel_id: ChannelId,
cx: &mut VisualTestContext,
) -> Task<anyhow::Result<View<ChannelView>>> {
) -> Task<anyhow::Result<Model<ChannelView>>> {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let view = window.root_view(cx).unwrap();
cx.update(|cx| ChannelView::open(channel_id, None, view.clone(), cx))
cx.update(|cx| ChannelView::open(channel_id, None, view.clone(), model, cx))
}
impl Drop for TestClient {

View File

@@ -11,9 +11,9 @@ use editor::{
EditorEvent,
};
use gpui::{
actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
WindowContext,
actions, AnyView, AppContext, AppContext, ClipboardItem, Entity as _, EventEmitter,
FocusableView, Model, Pixels, Point, Render, Subscription, Task, View, VisualContext as _,
WeakView,
};
use project::Project;
use rpc::proto::ChannelVisibility;
@@ -38,8 +38,8 @@ pub fn init(cx: &mut AppContext) {
}
pub struct ChannelView {
pub editor: View<Editor>,
workspace: WeakView<Workspace>,
pub editor: Model<Editor>,
workspace: WeakModel<Workspace>,
project: Model<Project>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
@@ -52,15 +52,17 @@ impl ChannelView {
pub fn open(
channel_id: ChannelId,
link_position: Option<String>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
workspace: Model<Workspace>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Model<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
let channel_view = Self::open_in_pane(
channel_id,
link_position,
pane.clone(),
workspace.clone(),
model,
cx,
);
cx.spawn(|mut cx| async move {
@@ -81,11 +83,12 @@ impl ChannelView {
pub fn open_in_pane(
channel_id: ChannelId,
link_position: Option<String>,
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let channel_view = Self::load(channel_id, workspace, cx);
pane: Model<Pane>,
workspace: Model<Workspace>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Model<Self>>> {
let channel_view = Self::load(channel_id, workspace, model, cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
@@ -101,7 +104,7 @@ impl ChannelView {
if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
{
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
existing_view.update(cx, |channel_view, model, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
@@ -120,8 +123,8 @@ impl ChannelView {
}
if let Some(link_position) = link_position {
channel_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
channel_view.update(cx, |channel_view, model, cx| {
channel_view.focus_position_from_link(link_position, true, model, cx)
});
}
@@ -132,24 +135,26 @@ impl ChannelView {
pub fn load(
channel_id: ChannelId,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
workspace: Model<Workspace>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Task<Result<Model<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = ChannelStore::global(cx);
let language_registry = workspace.app_state().languages.clone();
let markdown = language_registry.language_for_name("Markdown");
let channel_buffer =
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
let channel_buffer = channel_store.update(cx, |store, model, cx| {
store.open_channel_buffer(channel_id, model, cx)
});
cx.spawn(|mut cx| async move {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await.log_err();
channel_buffer.update(&mut cx, |channel_buffer, cx| {
channel_buffer.buffer().update(cx, |buffer, cx| {
channel_buffer.buffer().update(cx, |buffer, model, cx| {
buffer.set_language_registry(language_registry);
let Some(markdown) = markdown else {
return;
@@ -158,10 +163,16 @@ impl ChannelView {
})
})?;
cx.new_view(|cx| {
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
cx.new_model(|model, cx| {
let mut this = Self::new(
project,
weak_workspace,
channel_store,
channel_buffer,
model,
cx,
);
this.acknowledge_buffer_version(model, cx);
this
})
})
@@ -169,31 +180,39 @@ impl ChannelView {
pub fn new(
project: Model<Project>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
let this = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
let this = model.downgrade();
let editor = cx.new_model(|model, cx| {
let mut editor = Editor::for_buffer(buffer, None, model, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
)));
editor.set_custom_context_menu(move |_, position, cx| {
let this = this.clone();
Some(ui::ContextMenu::build(cx, move |menu, _| {
menu.entry("Copy link to section", None, move |cx| {
this.update(cx, |this, cx| this.copy_link_for_position(position, cx))
Some(ui::ContextMenu::build(
window,
cx,
move |menu, model, window, cx| {
menu.entry("Copy link to section", None, move |cx| {
this.update(cx, |this, model, cx| {
this.copy_link_for_position(position, cx)
})
.ok();
})
}))
})
},
))
});
editor
});
let _editor_event_subscription =
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| {
model.emit(e.clone(), cx)
});
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
.detach();
@@ -214,10 +233,13 @@ impl ChannelView {
&mut self,
position: String,
first_attempt: bool,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let position = Channel::slug(&position).to_lowercase();
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
let snapshot = self
.editor
.update(cx, |editor, model, cx| editor.snapshot(model, cx));
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
if let Some(item) = outline
@@ -225,8 +247,8 @@ impl ChannelView {
.iter()
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
self.editor.update(cx, |editor, model, cx| {
editor.change_selections(Some(Autoscroll::focused()), model, cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
})
});
@@ -254,15 +276,22 @@ impl ChannelView {
));
}
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
let position = self
.editor
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
self.copy_link_for_position(position, cx)
fn copy_link(&mut self, _: &CopyLink, model: &Model<Self>, cx: &mut AppContext) {
let position = self.editor.update(cx, |editor, model, cx| {
editor.selections.newest_display(cx).start
});
self.copy_link_for_position(position, model, cx)
}
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
fn copy_link_for_position(
&self,
position: DisplayPoint,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self
.editor
.update(cx, |editor, model, cx| editor.snapshot(model, cx));
let mut closest_heading = None;
@@ -282,7 +311,7 @@ impl ChannelView {
let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
cx.write_to_clipboard(ClipboardItem::new_string(link));
self.workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
struct CopyLinkForPositionToast;
workspace.show_toast(
@@ -304,29 +333,31 @@ impl ChannelView {
&mut self,
_: Model<ChannelBuffer>,
event: &ChannelBufferEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, model, cx| {
editor.set_read_only(true);
cx.notify();
model.notify(cx);
}),
ChannelBufferEvent::ChannelChanged => {
self.editor.update(cx, |_, cx| {
cx.emit(editor::EditorEvent::TitleChanged);
cx.notify()
self.editor.update(cx, |_, model, cx| {
model.emit(editor::EditorEvent::TitleChanged, cx);
model.notify(cx)
});
}
ChannelBufferEvent::BufferEdited => {
if self.editor.read(cx).is_focused(cx) {
self.acknowledge_buffer_version(cx);
if self.editor.read(cx).is_focused(window) {
self.acknowledge_buffer_version(model, cx);
} else {
self.channel_store.update(cx, |store, cx| {
self.channel_store.update(cx, |store, model, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.update_latest_notes_version(
channel_buffer.channel_id,
channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(),
model,
cx,
)
});
@@ -336,18 +367,19 @@ impl ChannelView {
}
}
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
self.channel_store.update(cx, |store, cx| {
fn acknowledge_buffer_version(&mut self, model: &Model<ChannelView>, cx: &mut AppContext) {
self.channel_store.update(cx, |store, model, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.acknowledge_notes_version(
channel_buffer.channel_id,
channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(),
model,
cx,
)
});
self.channel_buffer.update(cx, |buffer, cx| {
buffer.acknowledge_buffer_version(cx);
self.channel_buffer.update(cx, |buffer, model, cx| {
buffer.acknowledge_buffer_version(model, cx);
});
}
}
@@ -355,7 +387,12 @@ impl ChannelView {
impl EventEmitter<EditorEvent> for ChannelView {}
impl Render for ChannelView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
div()
.size_full()
.on_action(cx.listener(Self::copy_link))
@@ -375,19 +412,19 @@ impl Item for ChannelView {
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a View<Self>,
self_handle: &'a Model<Self>,
_: &'a AppContext,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.model())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.model())
} else {
None
}
}
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
fn tab_icon(&self, window: &Window, cx: &AppContext) -> Option<Icon> {
let channel = self.channel(cx)?;
let icon = match channel.visibility {
ChannelVisibility::Public => IconName::Public,
@@ -397,7 +434,12 @@ impl Item for ChannelView {
Some(Icon::new(icon))
}
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement {
fn tab_content(
&self,
params: TabContentParams,
window: &Window,
cx: &AppContext,
) -> gpui::AnyElement {
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
let status = match (
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
@@ -437,14 +479,16 @@ impl Item for ChannelView {
fn clone_on_split(
&self,
_: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
Some(cx.new_view(|cx| {
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Model<Self>> {
Some(cx.new_model(|model, cx| {
Self::new(
self.project.clone(),
self.workspace.clone(),
self.channel_store.clone(),
self.channel_buffer.clone(),
model,
cx,
)
}))
@@ -454,21 +498,27 @@ impl Item for ChannelView {
false
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
fn navigate(&mut self, data: Box<dyn Any>, model: &Model<Self>, cx: &mut AppContext) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))
.update(cx, |editor, model, cx| editor.navigate(data, model, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
fn deactivated(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.editor.update(cx, Item::deactivated)
}
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
fn set_nav_history(
&mut self,
history: ItemNavHistory,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.editor.update(cx, |editor, model, cx| {
Item::set_nav_history(editor, history, model, cx)
})
}
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, _: &Model<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
@@ -490,7 +540,7 @@ impl FollowableItem for ChannelView {
self.remote_id
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
fn to_state_proto(&self, window: &Window, cx: &AppContext) -> Option<proto::view::Variant> {
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
@@ -500,7 +550,7 @@ impl FollowableItem for ChannelView {
proto::view::ChannelView {
channel_id: channel_buffer.channel_id.0,
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
self.editor.read(cx).to_state_proto(model, cx)
{
Some(proto)
} else {
@@ -511,11 +561,12 @@ impl FollowableItem for ChannelView {
}
fn from_state_proto(
workspace: View<workspace::Workspace>,
workspace: Model<workspace::Workspace>,
remote_id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut WindowContext,
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Option<gpui::Task<anyhow::Result<Model<Self>>>> {
let Some(proto::view::Variant::ChannelView(_)) = state else {
return None;
};
@@ -523,16 +574,16 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
let open = ChannelView::load(ChannelId(state.channel_id), workspace, model, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
let task = this.update(&mut cx, |this, cx| {
let task = this.update(&mut cx, |this, model, cx| {
this.remote_id = Some(remote_id);
if let Some(state) = state.editor {
Some(this.editor.update(cx, |editor, cx| {
Some(this.editor.update(cx, |editor, model, cx| {
editor.apply_update_proto(
&this.project,
proto::update_view::Variant::Editor(proto::update_view::Editor {
@@ -543,6 +594,7 @@ impl FollowableItem for ChannelView {
scroll_y: state.scroll_y,
..Default::default()
}),
model,
cx,
)
}))
@@ -563,31 +615,38 @@ impl FollowableItem for ChannelView {
&self,
event: &EditorEvent,
update: &mut Option<proto::update_view::Variant>,
cx: &WindowContext,
window: &Window,
cx: &AppContext,
) -> bool {
self.editor
.read(cx)
.add_event_to_update_proto(event, update, cx)
.add_event_to_update_proto(event, update, model, cx)
}
fn apply_update_proto(
&mut self,
project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> gpui::Task<anyhow::Result<()>> {
self.editor.update(cx, |editor, cx| {
editor.apply_update_proto(project, message, cx)
self.editor.update(cx, |editor, model, cx| {
editor.apply_update_proto(project, message, model, cx)
})
}
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_leader_peer_id(leader_peer_id, cx)
fn set_leader_peer_id(
&mut self,
leader_peer_id: Option<PeerId>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.editor.update(cx, |editor, model, cx| {
editor.set_leader_peer_id(leader_peer_id, model, cx)
})
}
fn is_project_item(&self, _cx: &WindowContext) -> bool {
fn is_project_item(&self, _window: &Window, cx: &AppContext) -> bool {
false
}
@@ -595,7 +654,7 @@ impl FollowableItem for ChannelView {
Editor::to_follow_event(event)
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
fn dedup(&self, existing: &Self, window: &Window, cx: &AppContext) -> Option<Dedup> {
let existing = existing.channel_buffer.read(cx);
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
if existing.is_connected() {

View File

@@ -7,10 +7,10 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::{actions, Editor};
use gpui::{
actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, ClipboardItem,
CursorStyle, DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight,
HighlightStyle, ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription,
Task, View, ViewContext, VisualContext, WeakView,
actions, div, list, prelude::*, px, Action, AppContext, AppContext, ClipboardItem, CursorStyle,
DismissEvent, ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, HighlightStyle,
ListOffset, ListScrollEvent, ListState, Model, Render, Stateful, Subscription, Task, View,
VisualContext, WeakView,
};
use language::LanguageRegistry;
use menu::Confirm;
@@ -38,7 +38,7 @@ const CHAT_PANEL_KEY: &str = "ChatPanel";
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<ChatPanel>(cx);
});
})
@@ -51,7 +51,7 @@ pub struct ChatPanel {
languages: Arc<LanguageRegistry>,
message_list: ListState,
active_chat: Option<(Model<ChannelChat>, Subscription)>,
message_editor: View<MessageEditor>,
message_editor: Model<MessageEditor>,
local_timezone: UtcOffset,
fs: Arc<dyn Fs>,
width: Option<Pixels>,
@@ -74,30 +74,35 @@ struct SerializedChatPanel {
actions!(chat_panel, [ToggleFocus]);
impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
pub fn new(
workspace: &mut Workspace,
model: &Model<Workspace>,
cx: &mut AppContext,
) -> Model<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = ChannelStore::global(cx);
let user_store = workspace.app_state().user_store.clone();
let languages = workspace.app_state().languages.clone();
let input_editor = cx.new_view(|cx| {
let input_editor = cx.new_model(|model, cx| {
MessageEditor::new(
languages.clone(),
user_store.clone(),
None,
cx.new_view(|cx| Editor::auto_height(4, cx)),
cx.new_model(|model, cx| Editor::auto_height(4, model, cx)),
model,
cx,
)
});
cx.new_view(|cx: &mut ViewContext<Self>| {
let view = cx.view().downgrade();
cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let view = model.downgrade();
let message_list =
ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| {
view.render_message(ix, cx).into_any_element()
view.update(cx, |view, model, cx| {
view.render_message(ix, model, cx).into_any_element()
})
} else {
div().into_any()
@@ -127,7 +132,7 @@ impl ChatPanel {
active: false,
width: None,
markdown_data: Default::default(),
focus_handle: cx.focus_handle(),
focus_handle: window.focus_handle(),
open_context_menu: None,
highlighted_message: None,
last_acknowledged_message_id: None,
@@ -138,7 +143,7 @@ impl ChatPanel {
.room()
.and_then(|room| room.read(cx).channel_id())
{
this.select_channel(channel_id, None, cx)
this.select_channel(channel_id, None, model, cx)
.detach_and_log_err(cx);
}
@@ -147,7 +152,7 @@ impl ChatPanel {
move |this: &mut Self, call, event: &room::Event, cx| match event {
room::Event::RoomJoined { channel_id } => {
if let Some(channel_id) = channel_id {
this.select_channel(*channel_id, None, cx)
this.select_channel(*channel_id, None, model, cx)
.detach_and_log_err(cx);
if call
@@ -155,13 +160,13 @@ impl ChatPanel {
.room()
.is_some_and(|room| room.read(cx).contains_guests())
{
cx.emit(PanelEvent::Activate)
model.emit(PanelEvent::Activate, cx)
}
}
}
room::Event::RoomLeft { channel_id } => {
if channel_id == &this.channel_id(cx) {
cx.emit(PanelEvent::Close)
model.emit(PanelEvent::Close, cx)
}
}
_ => {}
@@ -187,9 +192,10 @@ impl ChatPanel {
}
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
workspace: WeakModel<Workspace>,
window: AnyWindowHandle,
cx: AsyncAppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
.background_executor()
@@ -204,11 +210,11 @@ impl ChatPanel {
};
workspace.update(&mut cx, |workspace, cx| {
let panel = Self::new(workspace, cx);
let panel = Self::new(workspace, model, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.update(cx, |panel, model, cx| {
panel.width = serialized_panel.width.map(|r| r.round());
cx.notify();
model.notify(cx);
});
}
panel
@@ -216,7 +222,7 @@ impl ChatPanel {
})
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
fn serialize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
@@ -232,18 +238,23 @@ impl ChatPanel {
);
}
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
fn set_active_chat(
&mut self,
chat: Model<ChannelChat>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
self.markdown_data.clear();
self.message_list.reset(chat.read(cx).message_count());
self.message_editor.update(cx, |editor, cx| {
editor.set_channel_chat(chat.clone(), cx);
self.message_editor.update(cx, |editor, model, cx| {
editor.set_channel_chat(chat.clone(), model, cx);
editor.clear_reply_to_message_id();
});
let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx);
cx.notify();
self.acknowledge_last_message(model, cx);
model.notify(cx);
}
}
@@ -251,7 +262,8 @@ impl ChatPanel {
&mut self,
_: Model<ChannelChat>,
event: &ChannelChatEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
ChannelChatEvent::MessagesUpdated {
@@ -260,7 +272,7 @@ impl ChatPanel {
} => {
self.message_list.splice(old_range.clone(), *new_count);
if self.active {
self.acknowledge_last_message(cx);
self.acknowledge_last_message(model, cx);
}
}
ChannelChatEvent::UpdateMessage {
@@ -275,16 +287,16 @@ impl ChatPanel {
message_id,
} => {
if !self.active {
self.channel_store.update(cx, |store, cx| {
store.update_latest_message_id(*channel_id, *message_id, cx)
self.channel_store.update(cx, |store, model, cx| {
store.update_latest_message_id(*channel_id, *message_id, model, cx)
})
}
}
}
cx.notify();
model.notify(cx);
}
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
fn acknowledge_last_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.active && self.is_scrolled_to_bottom {
if let Some((chat, _)) = &self.active_chat {
if let Some(channel_id) = self.channel_id(cx) {
@@ -294,8 +306,8 @@ impl ChatPanel {
.last_acknowledge_message_id(channel_id);
}
chat.update(cx, |chat, cx| {
chat.acknowledge_last_message(cx);
chat.update(cx, |chat, model, cx| {
chat.acknowledge_last_message(model, cx);
});
}
}
@@ -305,7 +317,8 @@ impl ChatPanel {
&mut self,
message_id: Option<ChannelMessageId>,
reply_to_message: &Option<ChannelMessage>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let reply_to_message = match reply_to_message {
None => {
@@ -369,8 +382,8 @@ impl ChatPanel {
),
)
.cursor(CursorStyle::PointingHand)
.tooltip(|cx| Tooltip::text("Go to message", cx))
.on_click(cx.listener(move |chat_panel, _, cx| {
.tooltip(|window, cx| Tooltip::text("Go to message", cx))
.on_click(model.listener(move |chat_panel, _, cx| {
if let Some(channel_id) = current_channel_id {
chat_panel
.select_channel(channel_id, reply_to_message_id.into(), cx)
@@ -380,10 +393,15 @@ impl ChatPanel {
)
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_message(
&mut self,
ix: usize,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let active_chat = &self.active_chat.as_ref().unwrap().0;
let (message, is_continuation_from_previous, is_admin) =
active_chat.update(cx, |active_chat, cx| {
active_chat.update(cx, |active_chat, model, cx| {
let is_admin = self
.channel_store
.read(cx)
@@ -481,6 +499,7 @@ impl ChatPanel {
el.child(self.render_replied_to_message(
Some(message.id),
&reply_to_message,
model,
cx,
))
.when(is_continuation_from_previous, |this| this.mt_2())
@@ -530,7 +549,7 @@ impl ChatPanel {
.w_full()
.text_ui_sm(cx)
.id(element_id)
.child(text.element("body".into(), cx)),
.child(text.element("body".into(), model, cx)),
)
.when(self.has_open_menu(message_id), |el| {
el.bg(cx.theme().colors().element_selected)
@@ -560,8 +579,14 @@ impl ChatPanel {
},
)
.child(
self.render_popover_buttons(cx, message_id, can_delete_message, can_edit_message)
.mt_neg_2p5(),
self.render_popover_buttons(
window,
cx,
message_id,
can_delete_message,
can_edit_message,
)
.mt_neg_2p5(),
)
}
@@ -572,7 +597,12 @@ impl ChatPanel {
}
}
fn render_popover_button(&self, cx: &ViewContext<Self>, child: Stateful<Div>) -> Div {
fn render_popover_button(
&self,
model: &Model<Self>,
cx: &AppContext,
child: Stateful<Div>,
) -> Div {
div()
.w_6()
.bg(cx.theme().colors().element_background)
@@ -582,7 +612,8 @@ impl ChatPanel {
fn render_popover_buttons(
&self,
cx: &ViewContext<Self>,
model: &Model<Self>,
cx: &AppContext,
message_id: Option<u64>,
can_delete_message: bool,
can_edit_message: bool,
@@ -601,21 +632,22 @@ impl ChatPanel {
.when_some(message_id, |el, message_id| {
el.child(
self.render_popover_button(
window,
cx,
div()
.id("reply")
.child(
IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
.on_click(cx.listener(move |this, _, cx| {
.on_click(model.listener(move |this, _, cx| {
this.cancel_edit_message(cx);
this.message_editor.update(cx, |editor, cx| {
this.message_editor.update(cx, |editor, model, cx| {
editor.set_reply_to_message_id(message_id);
editor.focus_handle(cx).focus(cx);
editor.focus_handle(cx).focus(window);
})
})),
)
.tooltip(|cx| Tooltip::text("Reply", cx)),
.tooltip(|window, cx| Tooltip::text("Reply", cx)),
),
)
})
@@ -623,13 +655,14 @@ impl ChatPanel {
el.when(can_edit_message, |el| {
el.child(
self.render_popover_button(
window,
cx,
div()
.id("edit")
.child(
IconButton::new(("edit", message_id), IconName::Pencil)
.on_click(cx.listener(move |this, _, cx| {
this.message_editor.update(cx, |editor, cx| {
.on_click(model.listener(move |this, _, cx| {
this.message_editor.update(cx, |editor, model, cx| {
editor.clear_reply_to_message_id();
let message = this
@@ -650,17 +683,17 @@ impl ChatPanel {
.as_singleton()
.expect("message editor must be singleton");
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.set_text(message.body.clone(), cx)
});
editor.set_edit_message_id(message_id);
editor.focus_handle(cx).focus(cx);
editor.focus_handle(cx).focus(window);
}
})
})),
)
.tooltip(|cx| Tooltip::text("Edit", cx)),
.tooltip(|window, cx| Tooltip::text("Edit", cx)),
),
)
})
@@ -670,6 +703,7 @@ impl ChatPanel {
el.child(
self.render_popover_button(
window,
cx,
div()
.child(
@@ -678,30 +712,32 @@ impl ChatPanel {
("trigger", message_id),
IconName::Ellipsis,
))
.menu(move |cx| {
.menu(move |window, cx| {
Some(Self::render_message_menu(
&this,
message_id,
can_delete_message,
model,
cx,
))
}),
)
.id("more")
.tooltip(|cx| Tooltip::text("More", cx)),
.tooltip(|window, cx| Tooltip::text("More", cx)),
),
)
})
}
fn render_message_menu(
this: &View<Self>,
this: &Model<Self>,
message_id: u64,
can_delete_message: bool,
cx: &mut WindowContext,
) -> View<ContextMenu> {
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) -> Model<ContextMenu> {
let menu = {
ContextMenu::build(cx, move |menu, cx| {
ContextMenu::build(window, cx, move |menu, model, window, _cx| {
menu.entry(
"Copy message text",
None,
@@ -723,7 +759,7 @@ impl ChatPanel {
})
})
};
this.update(cx, |this, cx| {
this.update(cx, |this, model, cx| {
let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
this.open_context_menu = None;
});
@@ -785,25 +821,27 @@ impl ChatPanel {
rich_text
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
fn send(&mut self, _: &Confirm, model: &Model<Self>, cx: &mut AppContext) {
if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self
.message_editor
.update(cx, |editor, cx| editor.take_message(cx));
.update(cx, |editor, model, cx| editor.take_message(model, cx));
if let Some(id) = self.message_editor.read(cx).edit_message_id() {
self.message_editor.update(cx, |editor, _| {
self.message_editor.update(cx, |editor, model, _| {
editor.clear_edit_message_id();
});
if let Some(task) = chat
.update(cx, |chat, cx| chat.update_message(id, message, cx))
.update(cx, |chat, model, cx| {
chat.update_message(id, message, model, cx)
})
.log_err()
{
task.detach();
}
} else if let Some(task) = chat
.update(cx, |chat, cx| chat.send_message(message, cx))
.update(cx, |chat, model, cx| chat.send_message(message, model, cx))
.log_err()
{
task.detach();
@@ -811,16 +849,18 @@ impl ChatPanel {
}
}
fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
fn remove_message(&mut self, id: u64, model: &Model<Self>, cx: &mut AppContext) {
if let Some((chat, _)) = self.active_chat.as_ref() {
chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
chat.update(cx, |chat, model, cx| {
chat.remove_message(id, model, cx).detach()
})
}
}
fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
fn load_more_messages(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some((chat, _)) = self.active_chat.as_ref() {
chat.update(cx, |channel, cx| {
if let Some(task) = channel.load_more_messages(cx) {
chat.update(cx, |channel, model, cx| {
if let Some(task) = channel.load_more_messages(model, cx) {
task.detach();
}
})
@@ -831,7 +871,8 @@ impl ChatPanel {
&mut self,
selected_channel_id: ChannelId,
scroll_to_message_id: Option<u64>,
cx: &mut ViewContext<ChatPanel>,
model: &Model<ChatPanel>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let open_chat = self
.active_chat
@@ -841,16 +882,16 @@ impl ChatPanel {
.then(|| Task::ready(anyhow::Ok(chat.clone())))
})
.unwrap_or_else(|| {
self.channel_store.update(cx, |store, cx| {
store.open_channel_chat(selected_channel_id, cx)
self.channel_store.update(cx, |store, model, cx| {
store.open_channel_chat(selected_channel_id, model, cx)
})
});
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
let chat = open_chat.await?;
let highlight_message_id = scroll_to_message_id;
let scroll_to_message_id = this.update(&mut cx, |this, cx| {
this.set_active_chat(chat.clone(), cx);
let scroll_to_message_id = this.update(&mut cx, |this, model, cx| {
this.set_active_chat(chat.clone(), model, cx);
scroll_to_message_id.or(this.last_acknowledged_message_id)
})?;
@@ -860,14 +901,14 @@ impl ChatPanel {
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
.await
{
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let Some(highlight_message_id) = highlight_message_id {
let task = cx.spawn({
|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.highlighted_message.take();
cx.notify();
model.notify(cx);
})
.ok();
}
@@ -881,7 +922,7 @@ impl ChatPanel {
item_ix,
offset_in_item: px(0.0),
});
cx.notify();
model.notify(cx);
}
})?;
}
@@ -891,13 +932,13 @@ impl ChatPanel {
})
}
fn close_reply_preview(&mut self, cx: &mut ViewContext<Self>) {
fn close_reply_preview(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.message_editor
.update(cx, |editor, _| editor.clear_reply_to_message_id());
.update(cx, |editor, model, _| editor.clear_reply_to_message_id());
}
fn cancel_edit_message(&mut self, cx: &mut ViewContext<Self>) {
self.message_editor.update(cx, |editor, cx| {
fn cancel_edit_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.message_editor.update(cx, |editor, model, cx| {
// only clear the editor input if we were editing a message
if editor.edit_message_id().is_none() {
return;
@@ -913,13 +954,18 @@ impl ChatPanel {
.as_singleton()
.expect("message editor must be singleton");
buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
buffer.update(cx, |buffer, model, cx| buffer.set_text("", model, cx));
});
}
}
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let channel_id = self
.active_chat
.as_ref()
@@ -971,6 +1017,7 @@ impl Render for ChatPanel {
.full_width()
.key_binding(KeyBinding::for_action(
&collab_panel::ToggleFocus,
model,
cx,
))
.on_click(|_, cx| {
@@ -999,8 +1046,8 @@ impl Render for ChatPanel {
.child(
IconButton::new("cancel-edit-message", IconName::Close)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel edit message", cx))
.on_click(cx.listener(move |this, _, cx| {
.tooltip(|window, cx| Tooltip::text("Cancel edit message", cx))
.on_click(model.listener(move |this, _, cx| {
this.cancel_edit_message(cx);
})),
),
@@ -1061,8 +1108,8 @@ impl Render for ChatPanel {
.child(
IconButton::new("close-reply-preview", IconName::Close)
.shape(ui::IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Close reply", cx))
.on_click(cx.listener(move |this, _, cx| {
.tooltip(|window, cx| Tooltip::text("Close reply", cx))
.on_click(model.listener(move |this, _, cx| {
this.close_reply_preview(cx);
})),
),
@@ -1096,7 +1143,7 @@ impl FocusableView for ChatPanel {
}
impl Panel for ChatPanel {
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
fn position(&self, window: &gpui::Window, cx: &gpui::AppContext) -> DockPosition {
ChatPanelSettings::get_global(cx).dock
}
@@ -1104,7 +1151,7 @@ impl Panel for ChatPanel {
matches!(position, DockPosition::Left | DockPosition::Right)
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
fn set_position(&mut self, position: DockPosition, model: &Model<Self>, cx: &mut AppContext) {
settings::update_settings_file::<ChatPanelSettings>(
self.fs.clone(),
cx,
@@ -1112,21 +1159,21 @@ impl Panel for ChatPanel {
);
}
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
fn size(&self, window: &gpui::Window, cx: &gpui::AppContext) -> Pixels {
self.width
.unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
fn set_size(&mut self, size: Option<Pixels>, model: &Model<Self>, cx: &mut AppContext) {
self.width = size;
self.serialize(cx);
cx.notify();
self.serialize(model, cx);
model.notify(cx);
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
fn set_active(&mut self, active: bool, model: &Model<Self>, cx: &mut AppContext) {
self.active = active;
if active {
self.acknowledge_last_message(cx);
self.acknowledge_last_message(model, cx);
}
}
@@ -1134,11 +1181,11 @@ impl Panel for ChatPanel {
"ChatPanel"
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
fn icon(&self, window: &Window, cx: &AppContext) -> Option<ui::IconName> {
Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
Some("Chat Panel")
}
@@ -1146,7 +1193,7 @@ impl Panel for ChatPanel {
Box::new(ToggleFocus)
}
fn starts_open(&self, cx: &WindowContext) -> bool {
fn starts_open(&self, window: &Window, cx: &AppContext) -> bool {
ActiveCall::global(cx)
.read(cx)
.room()

View File

@@ -5,8 +5,8 @@ use collections::HashSet;
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, Task, TextStyle, View, ViewContext, WeakView,
AppContext, AsyncAppContext, FontStyle, FontWeight, HighlightStyle, IntoElement, Model, Render,
Task, TextStyle, View, WeakView,
};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
@@ -37,7 +37,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
});
pub struct MessageEditor {
pub editor: View<Editor>,
pub editor: Model<Editor>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
mentions: Vec<UserId>,
@@ -46,7 +46,7 @@ pub struct MessageEditor {
edit_message_id: Option<u64>,
}
struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
struct MessageEditorCompletionProvider(WeakModel<MessageEditor>);
impl CompletionProvider for MessageEditorCompletionProvider {
fn completions(
@@ -54,12 +54,13 @@ impl CompletionProvider for MessageEditorCompletionProvider {
buffer: &Model<Buffer>,
buffer_position: language::Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) -> Task<anyhow::Result<Vec<Completion>>> {
let Some(handle) = self.0.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
handle.update(cx, |message_editor, cx| {
handle.update(cx, |message_editor, model, cx| {
message_editor.completions(buffer, buffer_position, cx)
})
}
@@ -69,7 +70,8 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_buffer: Model<Buffer>,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
_cx: &mut AppContext,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
}
@@ -79,7 +81,8 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_buffer: Model<Buffer>,
_completion: Completion,
_push_to_history: bool,
_cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
_cx: &mut AppContext,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
@@ -90,7 +93,8 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_cx: &mut ViewContext<Editor>,
model: &Model<Editor>,
_cx: &mut AppContext,
) -> bool {
text == "@"
}
@@ -101,16 +105,17 @@ impl MessageEditor {
language_registry: Arc<LanguageRegistry>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
editor: Model<Editor>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let this = cx.view().downgrade();
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
let this = model.downgrade();
editor.update(cx, |editor, model, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, model, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_gutter(false, model, cx);
editor.set_show_wrap_guides(false, model, cx);
editor.set_show_indent_guides(false, model, cx);
editor.set_completion_provider(Some(Box::new(MessageEditorCompletionProvider(this))));
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)
@@ -128,7 +133,7 @@ impl MessageEditor {
cx.subscribe(&buffer, Self::on_buffer_event).detach();
cx.observe_global::<settings::SettingsStore>(|view, cx| {
view.editor.update(cx, |editor, cx| {
view.editor.update(cx, |editor, model, cx| {
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)
.auto_replace_emoji_shortcode
@@ -182,24 +187,29 @@ impl MessageEditor {
self.edit_message_id = None;
}
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
pub fn set_channel_chat(
&mut self,
chat: Model<ChannelChat>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let channel_id = chat.read(cx).channel_id;
self.channel_chat = Some(chat);
let channel_name = ChannelStore::global(cx)
.read(cx)
.channel_for_id(channel_id)
.map(|channel| channel.name.clone());
self.editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, model, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{channel_name}"), cx);
editor.set_placeholder_text(format!("Message #{channel_name}"), model, cx);
} else {
editor.set_placeholder_text("Message Channel", cx);
editor.set_placeholder_text("Message Channel", model, cx);
}
});
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
self.editor.update(cx, |editor, cx| {
pub fn take_message(&mut self, model: &Model<Self>, cx: &mut AppContext) -> MessageParams {
self.editor.update(cx, |editor, model, cx| {
let highlights = editor.text_highlights::<Self>(cx);
let text = editor.text(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -213,7 +223,7 @@ impl MessageEditor {
Vec::new()
};
editor.clear(cx);
editor.clear(model, cx);
self.mentions.clear();
let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id);
@@ -229,15 +239,16 @@ impl MessageEditor {
&mut self,
buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
self.mentions_task = Some(model.spawn(cx, |this, cx| async move {
cx.background_executor()
.timer(MENTIONS_DEBOUNCE_INTERVAL)
.await;
Self::find_mentions(this, buffer, cx).await;
Self::find_mentions(this, buffer, model, cx).await;
}));
}
}
@@ -246,10 +257,11 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<Completion>>> {
if let Some((start_anchor, query, candidates)) =
self.collect_mention_candidates(buffer, end_anchor, cx)
self.collect_mention_candidates(buffer, end_anchor, model, cx)
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
@@ -266,7 +278,7 @@ impl MessageEditor {
}
if let Some((start_anchor, query, candidates)) =
self.collect_emoji_candidates(buffer, end_anchor, cx)
self.collect_emoji_candidates(buffer, end_anchor, model, cx)
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
@@ -286,7 +298,7 @@ impl MessageEditor {
}
async fn resolve_completions_for_candidates(
cx: &AsyncWindowContext,
cx: &AsyncAppContext,
query: &str,
candidates: &[StringMatchCandidate],
range: Range<Anchor>,
@@ -342,11 +354,12 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
let end_offset = end_anchor.to_offset(buffer.read(cx));
let query = buffer.update(cx, |buffer, _| {
let query = buffer.update(cx, |buffer, model, _| {
let mut query = String::new();
for ch in buffer.reversed_chars_at(end_offset).take(100) {
if ch == '@' {
@@ -395,7 +408,8 @@ impl MessageEditor {
&mut self,
buffer: &Model<Buffer>,
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
LazyLock::new(|| {
@@ -412,7 +426,7 @@ impl MessageEditor {
let end_offset = end_anchor.to_offset(buffer.read(cx));
let query = buffer.update(cx, |buffer, _| {
let query = buffer.update(cx, |buffer, model, _| {
let mut query = String::new();
for ch in buffer.reversed_chars_at(end_offset).take(100) {
if ch == ':' {
@@ -457,9 +471,10 @@ impl MessageEditor {
}
async fn find_mentions(
this: WeakView<MessageEditor>,
this: WeakModel<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncWindowContext,
mut window: AnyWindowHandle,
cx: AsyncAppContext,
) {
let (buffer, ranges) = cx
.background_executor()
@@ -469,12 +484,12 @@ impl MessageEditor {
})
.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let mut anchor_ranges = Vec::new();
let mut mentioned_user_ids = Vec::new();
let mut text = String::new();
this.editor.update(cx, |editor, cx| {
this.editor.update(cx, |editor, model, cx| {
let multi_buffer = editor.buffer().read(cx).snapshot(cx);
for range in ranges {
text.clear();
@@ -517,7 +532,12 @@ impl MessageEditor {
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@ use client::{
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, deferred, div, AppContext, ClipboardItem, DismissEvent, EventEmitter,
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
VisualContext, WeakView,
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, VisualContext,
WeakView,
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
@@ -26,7 +26,7 @@ actions!(
);
pub struct ChannelModal {
picker: View<Picker<ChannelModalDelegate>>,
picker: Model<Picker<ChannelModalDelegate>>,
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
}
@@ -37,11 +37,13 @@ impl ChannelModal {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
let channel_modal = cx.view().downgrade();
let picker = cx.new_view(|cx| {
cx.observe(&channel_store, |_, _, cx| model.notify(cx))
.detach();
let channel_modal = model.downgrade();
let picker = cx.new_model(|model, cx| {
Picker::uniform_list(
ChannelModalDelegate {
channel_modal,
@@ -57,6 +59,7 @@ impl ChannelModal {
has_all_members: false,
mode,
},
model,
cx,
)
.modal(false)
@@ -69,28 +72,33 @@ impl ChannelModal {
}
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
fn toggle_mode(&mut self, _: &ToggleMode, model: &Model<Self>, cx: &mut AppContext) {
let mode = match self.picker.read(cx).delegate.mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
self.set_mode(mode, cx);
self.set_mode(mode, model, cx);
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
fn set_mode(&mut self, mode: Mode, model: &Model<Self>, cx: &mut AppContext) {
self.picker.update(cx, |picker, model, cx| {
let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
picker.set_query("", model, cx);
picker.update_matches(picker.query(cx), model, cx);
model.notify(cx)
});
cx.notify()
model.notify(cx)
}
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
self.channel_store.update(cx, |channel_store, cx| {
fn set_channel_visibility(
&mut self,
selection: &Selection,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.channel_store.update(cx, |channel_store, model, cx| {
channel_store
.set_channel_visibility(
self.channel_id,
@@ -99,14 +107,15 @@ impl ChannelModal {
Selection::Selected => ChannelVisibility::Public,
Selection::Indeterminate => return,
},
model,
cx,
)
.detach_and_log_err(cx)
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
fn dismiss(&mut self, _: &menu::Cancel, model: &Model<Self>, cx: &mut AppContext) {
model.emit(DismissEvent, cx);
}
}
@@ -120,7 +129,12 @@ impl FocusableView for ChannelModal {
}
impl Render for ChannelModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
return div();
@@ -169,7 +183,7 @@ impl Render for ChannelModal {
Some(
Button::new("copy-link", "Copy Link")
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, cx| {
.on_click(model.listener(move |this, _, cx| {
if let Some(channel) = this
.channel_store
.read(cx)
@@ -197,7 +211,7 @@ impl Render for ChannelModal {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Manage Members"))
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.set_mode(Mode::ManageMembers, cx);
})),
)
@@ -212,7 +226,7 @@ impl Render for ChannelModal {
this.border_color(cx.theme().colors().border)
})
.child(Label::new("Invite Members"))
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.set_mode(Mode::InviteMembers, cx);
})),
),
@@ -229,7 +243,7 @@ pub enum Mode {
}
pub struct ChannelModalDelegate {
channel_modal: WeakView<ChannelModal>,
channel_modal: WeakModel<ChannelModal>,
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
user_store: Model<UserStore>,
@@ -240,13 +254,13 @@ pub struct ChannelModalDelegate {
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
has_all_members: bool,
context_menu: Option<(View<ContextMenu>, Subscription)>,
context_menu: Option<(Model<ContextMenu>, Subscription)>,
}
impl PickerDelegate for ChannelModalDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Search collaborator by username...".into()
}
@@ -261,11 +275,16 @@ impl PickerDelegate for ChannelModalDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &Model<Picker>, _: &mut AppContext) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
if self.has_all_members {
@@ -296,13 +315,13 @@ impl PickerDelegate for ChannelModalDelegate {
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
model.notify(cx);
})
.ok();
})
} else {
let search_members = self.channel_store.update(cx, |store, cx| {
store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
let search_members = self.channel_store.update(cx, |store, model, cx| {
store.fuzzy_search_members(self.channel_id, query.clone(), 100, model, cx)
});
cx.spawn(|picker, mut cx| async move {
async {
@@ -313,7 +332,7 @@ impl PickerDelegate for ChannelModalDelegate {
picker.delegate.matching_member_indices =
(0..members.len()).collect();
picker.delegate.members = members;
cx.notify();
model.notify(cx);
})?;
anyhow::Ok(())
}
@@ -323,15 +342,15 @@ impl PickerDelegate for ChannelModalDelegate {
}
}
Mode::InviteMembers => {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
let search_users = self.user_store.update(cx, |store, model, cx| {
store.fuzzy_search_users(query, model, cx)
});
cx.spawn(|picker, mut cx| async move {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate.matching_users = users;
cx.notify();
model.notify(cx);
})?;
anyhow::Ok(())
}
@@ -342,30 +361,30 @@ impl PickerDelegate for ChannelModalDelegate {
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(selected_user) = self.user_at_index(self.selected_index) {
if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id)
{
return;
}
match self.mode {
Mode::ManageMembers => self.show_context_menu(self.selected_index, cx),
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
Mode::ManageMembers => self.show_context_menu(self.selected_index, model, cx),
Mode::InviteMembers => match self.member_status(selected_user.id, model, cx) {
Some(proto::channel_member::Kind::Invitee) => {
self.remove_member(selected_user.id, cx);
self.remove_member(selected_user.id, model, cx);
}
Some(proto::channel_member::Kind::Member) => {}
None => self.invite_member(selected_user, cx),
None => self.invite_member(selected_user, model, cx),
},
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, model: &Model<Picker>, cx: &mut AppContext) {
if self.context_menu.is_none() {
self.channel_modal
.update(cx, |_, cx| {
cx.emit(DismissEvent);
.update(cx, |_, model, cx| {
model.emit(DismissEvent, cx);
})
.ok();
}
@@ -375,7 +394,8 @@ impl PickerDelegate for ChannelModalDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let user = self.user_at_index(ix)?;
let membership = self.member_at_index(ix);
@@ -474,10 +494,11 @@ impl ChannelModalDelegate {
&mut self,
user_id: UserId,
new_role: ChannelRole,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<()> {
let update = self.channel_store.update(cx, |store, cx| {
store.set_member_role(self.channel_id, user_id, new_role, cx)
let update = self.channel_store.update(cx, |store, model, cx| {
store.set_member_role(self.channel_id, user_id, new_role, model, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
@@ -487,16 +508,21 @@ impl ChannelModalDelegate {
member.role = new_role;
}
cx.focus_self();
cx.notify();
model.notify(cx);
})
})
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
Some(())
}
fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
let update = self.channel_store.update(cx, |store, cx| {
store.remove_member(self.channel_id, user_id, cx)
fn remove_member(
&mut self,
user_id: UserId,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<()> {
let update = self.channel_store.update(cx, |store, model, cx| {
store.remove_member(self.channel_id, user_id, model, cx)
});
cx.spawn(|picker, mut cx| async move {
update.await?;
@@ -518,52 +544,53 @@ impl ChannelModalDelegate {
.selected_index
.min(this.matching_member_indices.len().saturating_sub(1));
picker.focus(cx);
cx.notify();
picker.focus(window);
model.notify(cx);
})
})
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
Some(())
}
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
let invite_member = self.channel_store.update(cx, |store, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
fn invite_member(&mut self, user: Arc<User>, model: &Model<Picker>, cx: &mut AppContext) {
let invite_member = self.channel_store.update(cx, |store, model, cx| {
store.invite_member(self.channel_id, user.id, ChannelRole::Member, model, cx)
});
cx.spawn(|this, mut cx| async move {
invite_member.await?;
model
.spawn(cx, |this, mut cx| async move {
invite_member.await?;
this.update(&mut cx, |this, cx| {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
let members = &mut this.delegate.members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
this.update(&mut cx, |this, model, cx| {
let new_member = ChannelMembership {
user,
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
let members = &mut this.delegate.members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
cx.notify();
model.notify(cx);
})
})
})
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
}
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn show_context_menu(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
let Some(membership) = self.member_at_index(ix) else {
return;
};
let user_id = membership.user.id;
let picker = cx.view().clone();
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
let context_menu = ContextMenu::build(cx, window, |mut menu, _model, _window, _cx| {
let role = membership.role;
if role == ChannelRole::Admin || role == ChannelRole::Member {
let picker = picker.clone();
menu = menu.entry("Demote to Guest", None, move |cx| {
picker.update(cx, |picker, cx| {
picker.update(cx, |picker, model, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Guest, cx);
@@ -580,7 +607,7 @@ impl ChannelModalDelegate {
};
menu = menu.entry(label, None, move |cx| {
picker.update(cx, |picker, cx| {
picker.update(cx, |picker, model, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Member, cx);
@@ -591,7 +618,7 @@ impl ChannelModalDelegate {
if role == ChannelRole::Member || role == ChannelRole::Guest {
let picker = picker.clone();
menu = menu.entry("Promote to Admin", None, move |cx| {
picker.update(cx, |picker, cx| {
picker.update(cx, |picker, model, cx| {
picker
.delegate
.set_user_role(user_id, ChannelRole::Admin, cx);
@@ -603,7 +630,7 @@ impl ChannelModalDelegate {
menu = menu.entry("Remove from Channel", None, {
let picker = picker.clone();
move |cx| {
picker.update(cx, |picker, cx| {
picker.update(cx, |picker, model, cx| {
picker.delegate.remove_member(user_id, cx);
})
}
@@ -613,8 +640,8 @@ impl ChannelModalDelegate {
cx.focus_view(&context_menu);
let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
picker.delegate.context_menu = None;
picker.focus(cx);
cx.notify();
picker.focus(window);
model.notify(cx);
});
self.context_menu = Some((context_menu, subscription));
}

View File

@@ -1,7 +1,7 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _,
Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
AppContext, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement as _, Render, Styled, Task, View, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
@@ -10,31 +10,37 @@ use util::{ResultExt as _, TryFutureExt};
use workspace::ModalView;
pub struct ContactFinder {
picker: View<Picker<ContactFinderDelegate>>,
picker: Model<Picker<ContactFinderDelegate>>,
}
impl ContactFinder {
pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(user_store: Model<UserStore>, model: &Model<Self>, cx: &mut AppContext) -> Self {
let delegate = ContactFinderDelegate {
parent: cx.view().downgrade(),
parent: model.downgrade(),
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
};
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false));
let picker =
cx.new_model(|model, cx| Picker::uniform_list(delegate, model, cx).modal(false));
Self { picker }
}
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
pub fn set_query(&mut self, query: String, model: &Model<Self>, cx: &mut AppContext) {
self.picker.update(cx, |picker, model, cx| {
picker.set_query(query, cx);
});
}
}
impl Render for ContactFinder {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
v_flex()
.elevation_3(cx)
.child(
@@ -53,7 +59,7 @@ impl Render for ContactFinder {
}
pub struct ContactFinderDelegate {
parent: WeakView<ContactFinder>,
parent: WeakModel<ContactFinder>,
potential_contacts: Arc<[Arc<User>]>,
user_store: Model<UserStore>,
selected_index: usize,
@@ -79,25 +85,30 @@ impl PickerDelegate for ContactFinderDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &Model<Picker>, _: &mut AppContext) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Search collaborator by username...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
fn update_matches(
&mut self,
query: String,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Task<()> {
let search_users = self.user_store.update(cx, |store, model, cx| {
store.fuzzy_search_users(query, model, cx)
});
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate.potential_contacts = potential_contacts.into();
cx.notify();
model.notify(cx);
})?;
anyhow::Ok(())
}
@@ -106,18 +117,22 @@ impl PickerDelegate for ContactFinderDelegate {
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _: bool, model: &Model<Picker>, cx: &mut AppContext) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
self.user_store
.update(cx, |store, cx| store.request_contact(user.id, cx))
.update(cx, |store, model, cx| {
store.request_contact(user.id, model, cx)
})
.detach();
}
ContactRequestStatus::RequestSent => {
self.user_store
.update(cx, |store, cx| store.remove_contact(user.id, cx))
.update(cx, |store, model, cx| {
store.remove_contact(user.id, model, cx)
})
.detach();
}
_ => {}
@@ -125,9 +140,9 @@ impl PickerDelegate for ContactFinderDelegate {
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, model: &Model<Picker>, cx: &mut AppContext) {
self.parent
.update(cx, |_, cx| cx.emit(DismissEvent))
.update(cx, |_, model, cx| model.emit(DismissEvent, cx))
.log_err();
}
@@ -135,7 +150,8 @@ impl PickerDelegate for ContactFinderDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);

View File

@@ -6,11 +6,10 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
actions, div, img, list, px, AnyElement, AppContext, AsyncWindowContext, CursorStyle,
DismissEvent, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render,
StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView,
WindowContext,
actions, div, img, list, px, AnyElement, AppContext, AppContext, CursorStyle, DismissEvent,
Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement,
ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render,
StatefulInteractiveElement, Styled, Task, View, VisualContext, WeakView,
};
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs;
@@ -45,7 +44,7 @@ pub struct NotificationPanel {
notification_list: ListState,
pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
current_notification_toast: Option<(u64, Task<()>)>,
local_timezone: UtcOffset,
focus_handle: FocusHandle,
@@ -77,7 +76,7 @@ actions!(notification_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<NotificationPanel>(cx);
});
})
@@ -85,43 +84,51 @@ pub fn init(cx: &mut AppContext) {
}
impl NotificationPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
pub fn new(
workspace: &mut Workspace,
model: &Model<Workspace>,
cx: &mut AppContext,
) -> Model<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let user_store = workspace.app_state().user_store.clone();
let workspace_handle = workspace.weak_handle();
cx.new_view(|cx: &mut ViewContext<Self>| {
cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
let mut status = client.status();
cx.spawn(|this, mut cx| async move {
while (status.next().await).is_some() {
if this
.update(&mut cx, |_, cx| {
cx.notify();
})
.is_err()
{
break;
model
.spawn(cx, |this, mut cx| async move {
while (status.next().await).is_some() {
if this
.update(&mut cx, |_, cx| {
model.notify(cx);
})
.is_err()
{
break;
}
}
}
})
.detach();
})
.detach();
let view = cx.view().downgrade();
let view = model.downgrade();
let notification_list =
ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
view.upgrade()
.and_then(|view| {
view.update(cx, |this, cx| this.render_notification(ix, cx))
view.update(cx, |this, model, cx| {
this.render_notification(ix, model, cx)
})
})
.unwrap_or_else(|| div().into_any())
});
notification_list.set_scroll_handler(cx.listener(
|this, event: &ListScrollEvent, cx| {
if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
if let Some(task) = this
.notification_store
.update(cx, |store, cx| store.load_more_notifications(false, cx))
if let Some(task) =
this.notification_store.update(cx, |store, model, cx| {
store.load_more_notifications(false, cx)
})
{
task.detach();
}
@@ -140,7 +147,7 @@ impl NotificationPanel {
notification_list,
pending_serialization: Task::ready(None),
workspace: workspace_handle,
focus_handle: cx.focus_handle(),
focus_handle: window.focus_handle(),
current_notification_toast: None,
subscriptions: Vec::new(),
active: false,
@@ -151,15 +158,15 @@ impl NotificationPanel {
let mut old_dock_position = this.position(cx);
this.subscriptions.extend([
cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
cx.observe(&this.notification_store, |_, _, cx| model.notify(cx)),
cx.subscribe(&this.notification_store, Self::on_notification_event),
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(Event::DockPositionChanged);
model.emit(Event::DockPositionChanged, cx);
}
cx.notify();
model.notify(cx);
}),
]);
this
@@ -167,9 +174,10 @@ impl NotificationPanel {
}
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
workspace: WeakModel<Workspace>,
window: AnyWindowHandle,
cx: AsyncAppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
.background_executor()
@@ -184,11 +192,11 @@ impl NotificationPanel {
};
workspace.update(&mut cx, |workspace, cx| {
let panel = Self::new(workspace, cx);
let panel = Self::new(workspace, model, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.update(cx, |panel, model, cx| {
panel.width = serialized_panel.width.map(|w| w.round());
cx.notify();
model.notify(cx);
});
}
panel
@@ -196,7 +204,7 @@ impl NotificationPanel {
})
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
fn serialize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
@@ -212,7 +220,12 @@ impl NotificationPanel {
);
}
fn render_notification(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
fn render_notification(
&mut self,
ix: usize,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<AnyElement> {
let entry = self.notification_store.read(cx).notification_at(ix)?;
let notification_id = entry.id;
let now = OffsetDateTime::now_utc();
@@ -229,7 +242,7 @@ impl NotificationPanel {
let notification = entry.notification.clone();
if self.active && !entry.is_read {
self.did_render_notification(notification_id, &notification, cx);
self.did_render_notification(notification_id, &notification, model, cx);
}
let relative_timestamp = time_format::format_localized_timestamp(
@@ -259,7 +272,7 @@ impl NotificationPanel {
.when(can_navigate, |el| {
el.cursor(CursorStyle::PointingHand).on_click({
let notification = notification.clone();
cx.listener(move |this, _, cx| {
model.listener(move |this, _, cx| {
this.did_click_notification(&notification, cx)
})
})
@@ -288,7 +301,7 @@ impl NotificationPanel {
.rounded_md()
})
.child(Label::new(relative_timestamp).color(Color::Muted))
.tooltip(move |cx| {
.tooltip(move |window, cx| {
Tooltip::text(absolute_timestamp.clone(), cx)
}),
)
@@ -309,7 +322,7 @@ impl NotificationPanel {
let notification = notification.clone();
let view = cx.view().clone();
move |_, cx| {
view.update(cx, |this, cx| {
view.update(cx, |this, model, cx| {
this.respond_to_notification(
notification.clone(),
false,
@@ -322,7 +335,7 @@ impl NotificationPanel {
let notification = notification.clone();
let view = cx.view().clone();
move |_, cx| {
view.update(cx, |this, cx| {
view.update(cx, |this, model, cx| {
this.respond_to_notification(
notification.clone(),
true,
@@ -415,7 +428,8 @@ impl NotificationPanel {
&mut self,
notification_id: u64,
notification: &Notification,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let should_mark_as_read = match notification {
Notification::ContactRequestAccepted { .. } => true,
@@ -429,12 +443,12 @@ impl NotificationPanel {
.entry(notification_id)
.or_insert_with(|| {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
cx.background_executor().timer(MARK_AS_READ_DELAY).await;
client
.request(proto::MarkNotificationRead { notification_id })
.await?;
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, _, _| {
this.mark_as_read_tasks.remove(&notification_id);
})?;
Ok(())
@@ -443,7 +457,12 @@ impl NotificationPanel {
}
}
fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
fn did_click_notification(
&mut self,
notification: &Notification,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Notification::ChannelMessageMention {
message_id,
channel_id,
@@ -452,9 +471,9 @@ impl NotificationPanel {
{
if let Some(workspace) = self.workspace.upgrade() {
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.update(cx, |panel, model, cx| {
panel
.select_channel(ChannelId(channel_id), Some(message_id), cx)
.detach_and_log_err(cx);
@@ -466,7 +485,12 @@ impl NotificationPanel {
}
}
fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> bool {
fn is_showing_notification(
&self,
notification: &Notification,
model: &Model<Self>,
cx: &AppContext,
) -> bool {
if !self.active {
return false;
}
@@ -492,32 +516,33 @@ impl NotificationPanel {
&mut self,
_: Model<NotificationStore>,
event: &NotificationEvent,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
match event {
NotificationEvent::NewNotification { entry } => {
if !self.is_showing_notification(&entry.notification, cx) {
if !self.is_showing_notification(&entry.notification, model, cx) {
self.unseen_notifications.push(entry.clone());
}
self.add_toast(entry, cx);
self.add_toast(entry, model, cx);
}
NotificationEvent::NotificationRemoved { entry }
| NotificationEvent::NotificationRead { entry } => {
self.unseen_notifications.retain(|n| n.id != entry.id);
self.remove_toast(entry.id, cx);
self.remove_toast(entry.id, model, cx);
}
NotificationEvent::NotificationsUpdated {
old_range,
new_count,
} => {
self.notification_list.splice(old_range.clone(), *new_count);
cx.notify();
model.notify(cx);
}
}
}
fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
if self.is_showing_notification(&entry.notification, cx) {
fn add_toast(&mut self, entry: &NotificationEntry, model: &Model<Self>, cx: &mut AppContext) {
if self.is_showing_notification(&entry.notification, model, cx) {
return;
}
@@ -529,21 +554,23 @@ impl NotificationPanel {
let notification_id = entry.id;
self.current_notification_toast = Some((
notification_id,
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
cx.background_executor().timer(TOAST_DURATION).await;
this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
.ok();
this.update(&mut cx, |this, model, cx| {
this.remove_toast(notification_id, model, cx)
})
.ok();
}),
));
self.workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
let id = NotificationId::unique::<NotificationToast>();
workspace.dismiss_notification(&id, cx);
workspace.show_notification(id, cx, |cx| {
let workspace = cx.view().downgrade();
cx.new_view(|_| NotificationToast {
let workspace = model.downgrade();
cx.new_model(|_, _| NotificationToast {
notification_id,
actor,
text,
@@ -554,12 +581,12 @@ impl NotificationPanel {
.ok();
}
fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
fn remove_toast(&mut self, notification_id: u64, model: &Model<Self>, cx: &mut AppContext) {
if let Some((current_id, _)) = &self.current_notification_toast {
if *current_id == notification_id {
self.current_notification_toast.take();
self.workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
let id = NotificationId::unique::<NotificationToast>();
workspace.dismiss_notification(&id, cx)
})
@@ -572,16 +599,22 @@ impl NotificationPanel {
&mut self,
notification: Notification,
response: bool,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.notification_store.update(cx, |store, cx| {
store.respond_to_notification(notification, response, cx);
self.notification_store.update(cx, |store, model, cx| {
store.respond_to_notification(notification, response, model, cx);
});
}
}
impl Render for NotificationPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
v_flex()
.size_full()
.child(
@@ -662,7 +695,7 @@ impl Panel for NotificationPanel {
"NotificationPanel"
}
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
fn position(&self, cx: &gpui::AppContext) -> DockPosition {
NotificationPanelSettings::get_global(cx).dock
}
@@ -670,7 +703,7 @@ impl Panel for NotificationPanel {
matches!(position, DockPosition::Left | DockPosition::Right)
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
fn set_position(&mut self, position: DockPosition, model: &Model<Self>, cx: &mut AppContext) {
settings::update_settings_file::<NotificationPanelSettings>(
self.fs.clone(),
cx,
@@ -678,31 +711,31 @@ impl Panel for NotificationPanel {
);
}
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
fn size(&self, cx: &gpui::AppContext) -> Pixels {
self.width
.unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
fn set_size(&mut self, size: Option<Pixels>, model: &Model<Self>, cx: &mut AppContext) {
self.width = size;
self.serialize(cx);
cx.notify();
self.serialize(model, cx);
model.notify(cx);
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
fn set_active(&mut self, active: bool, model: &Model<Self>, cx: &mut AppContext) {
self.active = active;
if self.active {
self.unseen_notifications = Vec::new();
cx.notify();
model.notify(cx);
}
if self.notification_store.read(cx).notification_count() == 0 {
cx.emit(Event::Dismissed);
model.emit(Event::Dismissed, cx);
}
}
fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
fn icon(&self, cx: &gpui::AppContext) -> Option<IconName> {
let show_button = NotificationPanelSettings::get_global(cx).button;
if !show_button {
return None;
@@ -715,11 +748,11 @@ impl Panel for NotificationPanel {
Some(IconName::BellDot)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
fn icon_tooltip(&self, cx: &AppContext) -> Option<&'static str> {
Some("Notification Panel")
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
fn icon_label(&self, cx: &AppContext) -> Option<String> {
let count = self.notification_store.read(cx).unread_notification_count();
if count == 0 {
None
@@ -737,18 +770,18 @@ pub struct NotificationToast {
notification_id: u64,
actor: Option<Arc<User>>,
text: String,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
}
impl NotificationToast {
fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
fn focus_notification_panel(&self, model: &Model<Self>, cx: &mut AppContext) {
let workspace = self.workspace.clone();
let notification_id = self.notification_id;
cx.window_context().defer(move |cx| {
workspace
.update(cx, |workspace, cx| {
.update(cx, |workspace, model, cx| {
if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.update(cx, |panel, model, cx| {
let store = panel.notification_store.read(cx);
if let Some(entry) = store.notification_for_id(notification_id) {
panel.did_click_notification(&entry.clone().notification, cx);
@@ -762,7 +795,12 @@ impl NotificationToast {
}
impl Render for NotificationToast {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let user = self.actor.clone();
h_flex()
@@ -774,11 +812,11 @@ impl Render for NotificationToast {
.child(Label::new(self.text.clone()))
.child(
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
.on_click(cx.listener(|_, _, cx| model.emit(DismissEvent, cx))),
)
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.focus_notification_panel(cx);
cx.emit(DismissEvent);
model.emit(DismissEvent, cx);
}))
}
}

View File

@@ -32,7 +32,7 @@ impl ParentElement for CollabNotification {
}
impl RenderOnce for CollabNotification {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
h_flex()
.text_ui(cx)
.justify_between()

View File

@@ -37,7 +37,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
{
let window = cx
.open_window(options, |cx| {
cx.new_view(|_| {
cx.new_model(|_, _| {
IncomingCallNotification::new(
incoming_call.clone(),
app_state.clone(),
@@ -70,7 +70,8 @@ impl IncomingCallNotificationState {
fn respond(&self, accept: bool, cx: &mut AppContext) {
let active_call = ActiveCall::global(cx);
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let join =
active_call.update(cx, |active_call, model, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
@@ -95,7 +96,7 @@ impl IncomingCallNotificationState {
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, cx| {
active_call.update(cx, |active_call, model, cx| {
active_call.decline_incoming(cx).log_err();
});
}
@@ -111,7 +112,12 @@ impl IncomingCallNotification {
}
impl Render for IncomingCallNotification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let ui_font = theme::setup_ui_font(cx);
div().size_full().font(ui_font).child(

View File

@@ -29,7 +29,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let options = notification_window_options(screen, window_size, cx);
let Some(window) = cx
.open_window(options, |cx| {
cx.new_view(|_| {
cx.new_model(|_, _| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
@@ -55,7 +55,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(windows) = notification_windows.remove(project_id) {
for window in windows {
window
.update(cx, |_, cx| {
.update(cx, |_, model, cx| {
cx.remove_window();
})
.ok();
@@ -67,7 +67,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
for (_, windows) in notification_windows.drain() {
for window in windows {
window
.update(cx, |_, cx| {
.update(cx, |_, model, cx| {
cx.remove_window();
})
.ok();
@@ -101,39 +101,53 @@ impl ProjectSharedNotification {
}
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
fn join(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
active_room.update(cx, |_, model, cx| {
model.emit(
cx,
room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
},
);
});
}
}
}
impl Render for ProjectSharedNotification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let ui_font = theme::setup_ui_font(cx);
div().size_full().font(ui_font).child(
CollabNotification::new(
self.owner.avatar_uri.clone(),
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
this.join(cx);
})),
Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
this.dismiss(cx);
})),
Button::new("open", "Open").on_click(model.listener(
cx,
move |this, _event, cx| {
this.join(cx);
},
)),
Button::new("dismiss", "Dismiss").on_click(model.listener(
cx,
move |this, _event, cx| {
this.dismiss(cx);
},
)),
)
.child(Label::new(self.owner.github_login.clone()))
.child(Label::new(format!(

View File

@@ -7,7 +7,12 @@ use crate::notifications::collab_notification::CollabNotification;
pub struct CollabNotificationStory;
impl Render for CollabNotificationStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
_window: &mut Window,
_cx: &mut AppContext,
) -> impl IntoElement {
let window_container = |width, height| div().w(px(width)).h(px(height));
Story::container()

View File

@@ -11,8 +11,8 @@ use command_palette_hooks::{
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
Action, AppContext, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, Task, UpdateGlobal, View, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@@ -33,7 +33,7 @@ pub fn init(cx: &mut AppContext) {
impl ModalView for CommandPalette {}
pub struct CommandPalette {
picker: View<Picker<CommandPaletteDelegate>>,
picker: Model<Picker<CommandPaletteDelegate>>,
}
fn trim_consecutive_whitespaces(input: &str) -> String {
@@ -55,17 +55,24 @@ fn trim_consecutive_whitespaces(input: &str) -> String {
}
impl CommandPalette {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &Toggle, cx| Self::toggle(workspace, "", cx));
fn register(workspace: &mut Workspace, _: &Model<Workspace>, _: &mut AppContext) {
workspace.register_action(model, |workspace, _: &Toggle, cx| {
Self::toggle(workspace, "", model, cx)
});
}
pub fn toggle(workspace: &mut Workspace, query: &str, cx: &mut ViewContext<Workspace>) {
pub fn toggle(
workspace: &mut Workspace,
query: &str,
model: &Model<Workspace>,
cx: &mut AppContext,
) {
let Some(previous_focus_handle) = cx.focused() else {
return;
};
let telemetry = workspace.client().telemetry().clone();
workspace.toggle_modal(cx, move |cx| {
CommandPalette::new(previous_focus_handle, telemetry, query, cx)
workspace.toggle_modal(window, cx, move |cx| {
CommandPalette::new(previous_focus_handle, telemetry, query, model, cx)
});
}
@@ -73,7 +80,8 @@ impl CommandPalette {
previous_focus_handle: FocusHandle,
telemetry: Arc<Telemetry>,
query: &str,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let filter = CommandPaletteFilter::try_global(cx);
@@ -93,23 +101,23 @@ impl CommandPalette {
.collect();
let delegate = CommandPaletteDelegate::new(
cx.view().downgrade(),
model.downgrade(),
commands,
telemetry,
previous_focus_handle,
);
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx);
picker.set_query(query, cx);
let picker = cx.new_model(|model, cx| {
let picker = Picker::uniform_list(delegate, model, cx);
picker.set_query(query, model, cx);
picker
});
Self { picker }
}
pub fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
pub fn set_query(&mut self, query: &str, model: &Model<Self>, cx: &mut AppContext) {
self.picker
.update(cx, |picker, cx| picker.set_query(query, cx))
.update(cx, |picker, model, cx| picker.set_query(query, cx))
}
}
@@ -122,13 +130,13 @@ impl FocusableView for CommandPalette {
}
impl Render for CommandPalette {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, model: &Model<Self>, _cx: &mut AppContext) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
pub struct CommandPaletteDelegate {
command_palette: WeakView<CommandPalette>,
command_palette: WeakModel<CommandPalette>,
all_commands: Vec<Command>,
commands: Vec<Command>,
matches: Vec<StringMatch>,
@@ -165,7 +173,7 @@ impl Global for HitCounts {}
impl CommandPaletteDelegate {
fn new(
command_palette: WeakView<CommandPalette>,
command_palette: WeakModel<CommandPalette>,
commands: Vec<Command>,
telemetry: Arc<Telemetry>,
previous_focus_handle: FocusHandle,
@@ -187,7 +195,8 @@ impl CommandPaletteDelegate {
query: String,
mut commands: Vec<Command>,
mut matches: Vec<StringMatch>,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) {
self.updating_matches.take();
@@ -241,7 +250,7 @@ impl CommandPaletteDelegate {
impl PickerDelegate for CommandPaletteDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
"Execute a command...".into()
}
@@ -253,14 +262,15 @@ impl PickerDelegate for CommandPaletteDelegate {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _: &Model<Picker>, _: &mut AppContext) {
self.selected_ix = ix;
}
fn update_matches(
&mut self,
mut query: String,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> gpui::Task<()> {
let settings = WorkspaceSettings::get_global(cx);
if let Some(alias) = settings.command_aliases.get(&query) {
@@ -336,7 +346,8 @@ impl PickerDelegate for CommandPaletteDelegate {
&mut self,
query: String,
duration: Duration,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> bool {
let Some((task, rx)) = self.updating_matches.take() else {
return true;
@@ -347,7 +358,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.block_with_timeout(duration, rx.clone().recv())
{
Ok(Some((commands, matches))) => {
self.matches_updated(query, commands, matches, cx);
self.matches_updated(query, commands, matches, model, cx);
true
}
_ => {
@@ -357,15 +368,15 @@ impl PickerDelegate for CommandPaletteDelegate {
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
fn dismissed(&mut self, model: &Model<Picker>, cx: &mut AppContext) {
self.command_palette
.update(cx, |_, cx| cx.emit(DismissEvent))
.update(cx, |_, model, cx| model.emit(DismissEvent, cx))
.log_err();
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, _: bool, model: &Model<Picker>, cx: &mut AppContext) {
if self.matches.is_empty() {
self.dismissed(cx);
self.dismissed(model, cx);
return;
}
let action_ix = self.matches[self.selected_ix].candidate_id;
@@ -381,15 +392,16 @@ impl PickerDelegate for CommandPaletteDelegate {
});
let action = command.action;
cx.focus(&self.previous_focus_handle);
self.dismissed(cx);
cx.dispatch_action(action);
self.dismissed(model, cx);
model.dispatch_action(cx, action);
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
model: &Model<Picker>,
cx: &mut AppContext,
) -> Option<Self::ListItem> {
let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?;
@@ -410,6 +422,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.children(KeyBinding::for_action_in(
&*command.action,
&self.previous_focus_handle,
window,
cx,
)),
),
@@ -482,22 +495,23 @@ mod tests {
async fn test_command_palette(cx: &mut TestAppContext) {
let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let (workspace, model, cx) =
cx.add_window_view(|cx| Workspace::test_new(project.clone(), model, cx));
let editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_text("abc", cx);
let editor = cx.new_model(|model, cx| {
let mut editor = Editor::single_line(model, cx);
editor.set_text("abc", model, cx);
editor
});
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
editor.update(cx, |editor, cx| editor.focus(cx))
editor.update(cx, |editor, model, cx| editor.focus(window, cx))
});
cx.simulate_keystrokes("cmd-shift-p");
let palette = workspace.update(cx, |workspace, cx| {
let palette = workspace.update(cx, |workspace, model, cx| {
workspace
.active_modal::<CommandPalette>(cx)
.unwrap()
@@ -506,7 +520,7 @@ mod tests {
.clone()
});
palette.update(cx, |palette, _| {
palette.update(cx, |palette, model, _| {
assert!(palette.delegate.commands.len() > 5);
let is_sorted =
|actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
@@ -515,13 +529,13 @@ mod tests {
cx.simulate_input("bcksp");
palette.update(cx, |palette, _| {
palette.update(cx, |palette, model, _| {
assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
});
cx.simulate_keystrokes("enter");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
assert_eq!(editor.read(cx).text(cx), "ab")
});
@@ -536,7 +550,7 @@ mod tests {
cx.simulate_keystrokes("cmd-shift-p");
cx.simulate_input("bcksp");
let palette = workspace.update(cx, |workspace, cx| {
let palette = workspace.update(cx, |workspace, model, cx| {
workspace
.active_modal::<CommandPalette>(cx)
.unwrap()
@@ -544,7 +558,7 @@ mod tests {
.picker
.clone()
});
palette.update(cx, |palette, _| {
palette.update(cx, |palette, model, _| {
assert!(palette.delegate.matches.is_empty())
});
}
@@ -553,27 +567,30 @@ mod tests {
async fn test_go_to_line(cx: &mut TestAppContext) {
let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let (workspace, cx) =
cx.add_window_view(|cx| Workspace::test_new(project.clone(), model, cx));
cx.simulate_keystrokes("cmd-n");
let editor = workspace.update(cx, |workspace, cx| {
let editor = workspace.update(cx, |workspace, model, cx| {
workspace.active_item_as::<Editor>(cx).unwrap()
});
editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx));
editor.update(cx, |editor, model, cx| {
editor.set_text("1\n2\n3\n4\n5\n6\n", cx)
});
cx.simulate_keystrokes("cmd-shift-p");
cx.simulate_input("go to line: Toggle");
cx.simulate_keystrokes("enter");
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
assert!(workspace.active_modal::<GoToLine>(cx).is_some())
});
cx.simulate_keystrokes("3 enter");
editor.update(cx, |editor, cx| {
assert!(editor.focus_handle(cx).is_focused(cx));
editor.update(cx, |editor, model, cx| {
assert!(editor.focus_handle(cx).is_focused(window));
assert_eq!(
editor.selections.last::<Point>(cx).range().start,
Point::new(2, 0)

View File

@@ -51,8 +51,8 @@ impl Tool for ContextServerTool {
fn run(
self: std::sync::Arc<Self>,
input: serde_json::Value,
_workspace: gpui::WeakView<workspace::Workspace>,
cx: &mut ui::WindowContext,
_workspace: gpui::WeakModel<workspace::Workspace>,
cx: &mut model, ui::
) -> gpui::Task<gpui::Result<String>> {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
cx.foreground_executor().spawn({

View File

@@ -34,7 +34,7 @@ impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
cx: &mut AppContext,
) {
self.context_server_factory_registry
.update(cx, |registry, _| {
.update(cx, |registry, model, _| {
registry.register_server_factory(
id.clone(),
Arc::new({

View File

@@ -20,7 +20,7 @@ use std::sync::Arc;
use anyhow::{bail, Result};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel};
use gpui::{AsyncAppContext, EventEmitter, Model, Subscription, Task, WeakModel};
use log;
use parking_lot::RwLock;
use project::Project;
@@ -122,7 +122,8 @@ impl ContextServerManager {
pub fn new(
registry: Model<ContextServerFactoryRegistry>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let mut this = Self {
_subscriptions: vec![
@@ -143,18 +144,18 @@ impl ContextServerManager {
this
}
fn available_context_servers_changed(&mut self, cx: &mut ModelContext<Self>) {
fn available_context_servers_changed(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.update_servers_task.is_some() {
self.needs_server_update = true;
} else {
self.update_servers_task = Some(cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |this, _| {
self.update_servers_task = Some(model.spawn(cx, |this, mut cx| async move {
this.update(&mut cx, |this, _, _| {
this.needs_server_update = false;
})?;
Self::maintain_servers(this.clone(), cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
let has_any_context_servers = !this.servers().is_empty();
if has_any_context_servers {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -183,23 +184,30 @@ impl ContextServerManager {
pub fn restart_server(
&mut self,
id: &Arc<str>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<anyhow::Result<()>> {
let id = id.clone();
cx.spawn(|this, mut cx| async move {
model.spawn(cx, |this, mut cx| async move {
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
server.stop()?;
let config = server.config();
let new_server = Arc::new(ContextServer::new(id.clone(), config));
new_server.clone().start(&cx).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.servers.insert(id.clone(), new_server);
cx.emit(Event::ServerStopped {
server_id: id.clone(),
});
cx.emit(Event::ServerStarted {
server_id: id.clone(),
});
model.emit(
cx,
Event::ServerStopped {
server_id: id.clone(),
},
);
model.emit(
cx,
Event::ServerStarted {
server_id: id.clone(),
},
);
})?;
}
Ok(())
@@ -217,7 +225,7 @@ impl ContextServerManager {
async fn maintain_servers(this: WeakModel<Self>, mut cx: AsyncAppContext) -> Result<()> {
let mut desired_servers = HashMap::default();
let (registry, project) = this.update(&mut cx, |this, cx| {
let (registry, project) = this.update(&mut cx, |this, model, cx| {
let location = this.project.read(cx).worktrees(cx).next().map(|worktree| {
settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
@@ -271,14 +279,14 @@ impl ContextServerManager {
for (id, server) in servers_to_stop {
server.stop().log_err();
this.update(&mut cx, |_, cx| {
cx.emit(Event::ServerStopped { server_id: id })
model.emit(Event::ServerStopped { server_id: id }, cx)
})?;
}
for (id, server) in servers_to_start {
if server.start(&cx).await.log_err().is_some() {
this.update(&mut cx, |_, cx| {
cx.emit(Event::ServerStarted { server_id: id })
model.emit(Event::ServerStarted { server_id: id }, cx)
})?;
}
}

View File

@@ -31,7 +31,7 @@ impl ContextServerFactoryRegistry {
/// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Model<Self> {
if !cx.has_global::<GlobalContextServerFactoryRegistry>() {
let registry = cx.new_model(|_| Self::new());
let registry = cx.new_model(|_, _| Self::new());
cx.set_global(GlobalContextServerFactoryRegistry(registry));
}
cx.global::<GlobalContextServerFactoryRegistry>().0.clone()

View File

@@ -12,7 +12,7 @@ use command_palette_hooks::CommandPaletteFilter;
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{
actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
ModelContext, Task, WeakModel,
Task, WeakModel,
};
use http_client::github::get_release_by_tag_name;
use http_client::HttpClient;
@@ -64,7 +64,7 @@ pub fn init(
let copilot = cx.new_model({
let node_runtime = node_runtime.clone();
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
move |cx| Copilot::start(new_server_id, http, node_runtime, model, cx)
});
Copilot::set_global(copilot.clone(), cx);
cx.observe(&copilot, |handle, cx| {
@@ -105,21 +105,21 @@ pub fn init(
cx.on_action(|_: &SignIn, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.update(cx, |copilot, model, cx| copilot.sign_in(model, cx))
.detach_and_log_err(cx);
}
});
cx.on_action(|_: &SignOut, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.update(cx, |copilot, model, cx| copilot.sign_out(model, cx))
.detach_and_log_err(cx);
}
});
cx.on_action(|_: &Reinstall, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.reinstall(cx))
.update(cx, |copilot, model, cx| copilot.reinstall(model, cx))
.detach();
}
});
@@ -210,7 +210,8 @@ impl RegisteredBuffer {
fn report_changes(
&mut self,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Copilot>,
model: &Model<Copilot>,
cx: &mut AppContext,
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
let (done_tx, done_rx) = oneshot::channel();
@@ -335,7 +336,8 @@ impl Copilot {
new_server_id: LanguageServerId,
http: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let mut this = Self {
server_id: new_server_id,
@@ -343,9 +345,9 @@ impl Copilot {
node_runtime,
server: CopilotServer::Disabled,
buffers: Default::default(),
_subscription: cx.on_app_quit(Self::shutdown_language_server),
_subscription: model.on_app_quit(cx, Self::shutdown_language_server),
};
this.enable_or_disable_copilot(cx);
this.enable_or_disable_copilot(model, cx);
cx.observe_global::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
.detach();
this
@@ -353,7 +355,8 @@ impl Copilot {
fn shutdown_language_server(
&mut self,
_cx: &mut ModelContext<Self>,
model: &Model<Self>,
_cx: &mut AppContext,
) -> impl Future<Output = ()> {
let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) {
CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })),
@@ -367,7 +370,7 @@ impl Copilot {
}
}
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Self>) {
fn enable_or_disable_copilot(&mut self, model: &Model<Self>, cx: &mut AppContext) {
let server_id = self.server_id;
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
@@ -381,11 +384,11 @@ impl Copilot {
})
.shared();
self.server = CopilotServer::Starting { task: start_task };
cx.notify();
model.notify(cx);
}
} else {
self.server = CopilotServer::Disabled;
cx.notify();
model.notify(cx);
}
}
@@ -407,7 +410,7 @@ impl Copilot {
);
let http = http_client::FakeHttpClient::create(|_| async { unreachable!() });
let node_runtime = NodeRuntime::unavailable();
let this = cx.new_model(|cx| Self {
let this = cx.new_model(|model, cx| Self {
server_id: LanguageServerId(0),
http: http.clone(),
node_runtime,
@@ -416,7 +419,7 @@ impl Copilot {
sign_in_status: SignInStatus::Authorized,
registered_buffers: Default::default(),
}),
_subscription: cx.on_app_quit(Self::shutdown_language_server),
_subscription: model.on_app_quit(cx, Self::shutdown_language_server),
buffers: Default::default(),
});
(this, fake_server)
@@ -485,8 +488,8 @@ impl Copilot {
};
let server = start_language_server.await;
this.update(&mut cx, |this, cx| {
cx.notify();
this.update(&mut cx, |this, model, cx| {
model.notify(cx);
match server {
Ok((server, status)) => {
this.server = CopilotServer::Running(RunningCopilotServer {
@@ -494,24 +497,24 @@ impl Copilot {
sign_in_status: SignInStatus::SignedOut,
registered_buffers: Default::default(),
});
cx.emit(Event::CopilotLanguageServerStarted);
this.update_sign_in_status(status, cx);
model.emit(Event::CopilotLanguageServerStarted, cx);
this.update_sign_in_status(status, model, cx);
}
Err(error) => {
this.server = CopilotServer::Error(error.to_string().into());
cx.notify()
model.notify(cx)
}
}
})
.ok();
}
pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn sign_in(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
SignInStatus::SigningIn { task, .. } => {
cx.notify();
model.notify(cx);
task.clone()
}
SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
@@ -529,7 +532,7 @@ impl Copilot {
Ok(request::SignInStatus::Ok { user: Some(user) })
}
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if let CopilotServer::Running(RunningCopilotServer {
sign_in_status: status,
..
@@ -541,7 +544,7 @@ impl Copilot {
} = status
{
*prompt_flow = Some(flow.clone());
cx.notify();
model.notify(cx);
}
}
})?;
@@ -558,7 +561,7 @@ impl Copilot {
};
let sign_in = sign_in.await;
this.update(&mut cx, |this, cx| match sign_in {
this.update(&mut cx, |this, model, cx| match sign_in {
Ok(status) => {
this.update_sign_in_status(status, cx);
Ok(())
@@ -577,7 +580,7 @@ impl Copilot {
prompt: None,
task: task.clone(),
};
cx.notify();
model.notify(cx);
task
}
};
@@ -591,8 +594,8 @@ impl Copilot {
}
}
pub fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
pub fn sign_out(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
self.update_sign_in_status(request::SignInStatus::NotSignedIn, model, cx);
if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
let server = server.clone();
cx.background_executor().spawn(async move {
@@ -606,7 +609,7 @@ impl Copilot {
}
}
pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
pub fn reinstall(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<()> {
let start_task = cx
.spawn({
let http = self.http.clone();
@@ -623,7 +626,7 @@ impl Copilot {
task: start_task.clone(),
};
cx.notify();
model.notify(cx);
cx.background_executor().spawn(start_task)
}
@@ -636,7 +639,12 @@ impl Copilot {
}
}
pub fn register_buffer(&mut self, buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) {
pub fn register_buffer(
&mut self,
buffer: &Model<Buffer>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let weak_buffer = buffer.downgrade();
self.buffers.insert(weak_buffer.clone());
@@ -694,14 +702,15 @@ impl Copilot {
&mut self,
buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Result<()> {
if let Ok(server) = self.server.as_running() {
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
{
match event {
language::BufferEvent::Edited => {
drop(registered_buffer.report_changes(&buffer, cx));
drop(registered_buffer.report_changes(&buffer, model, cx));
}
language::BufferEvent::Saved => {
server
@@ -772,30 +781,33 @@ impl Copilot {
&mut self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<Completion>>>
where
T: ToPointUtf16,
{
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
self.request_completions::<request::GetCompletions, _>(buffer, position, model, cx)
}
pub fn completions_cycling<T>(
&mut self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<Completion>>>
where
T: ToPointUtf16,
{
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, model, cx)
}
pub fn accept_completion(
&mut self,
completion: &Completion,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
@@ -816,7 +828,8 @@ impl Copilot {
pub fn discard_completions(
&mut self,
completions: &[Completion],
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
@@ -841,7 +854,8 @@ impl Copilot {
&mut self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<Vec<Completion>>>
where
R: 'static
@@ -851,7 +865,7 @@ impl Copilot {
>,
T: ToPointUtf16,
{
self.register_buffer(buffer, cx);
self.register_buffer(buffer, model, cx);
let server = match self.server.as_authenticated() {
Ok(server) => server,
@@ -862,7 +876,7 @@ impl Copilot {
.registered_buffers
.get_mut(&buffer.entity_id())
.unwrap();
let snapshot = registered_buffer.report_changes(buffer, cx);
let snapshot = registered_buffer.report_changes(buffer, model, cx);
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer);
@@ -933,7 +947,8 @@ impl Copilot {
fn update_sign_in_status(
&mut self,
lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.buffers.retain(|buffer| buffer.is_upgradable());
@@ -943,10 +958,10 @@ impl Copilot {
| request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => {
server.sign_in_status = SignInStatus::Authorized;
cx.emit(Event::CopilotAuthSignedIn);
model.emit(Event::CopilotAuthSignedIn, cx);
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
if let Some(buffer) = buffer.upgrade() {
self.register_buffer(&buffer, cx);
self.register_buffer(&buffer, model, cx);
}
}
}
@@ -958,14 +973,14 @@ impl Copilot {
}
request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
cx.emit(Event::CopilotAuthSignedOut);
model.emit(Event::CopilotAuthSignedOut, cx);
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);
}
}
}
cx.notify();
model.notify(cx);
}
}
}
@@ -1066,11 +1081,13 @@ mod tests {
async fn test_buffer_management(cx: &mut TestAppContext) {
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.new_model(|cx| Buffer::local("Hello", cx));
let buffer_1 = cx.new_model(|model, cx| Buffer::local("Hello", model, cx));
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
copilot.update(cx, |copilot, model, cx| {
copilot.register_buffer(&buffer_1, model, cx)
});
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
@@ -1084,11 +1101,13 @@ mod tests {
}
);
let buffer_2 = cx.new_model(|cx| Buffer::local("Goodbye", cx));
let buffer_2 = cx.new_model(|model, cx| Buffer::local("Goodbye", model, cx));
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
copilot.update(cx, |copilot, model, cx| {
copilot.register_buffer(&buffer_2, model, cx)
});
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
@@ -1102,7 +1121,9 @@ mod tests {
}
);
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
buffer_1.update(cx, |buffer, model, cx| {
buffer.edit([(5..5, " world")], None, model, cx)
});
assert_eq!(
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
.await,
@@ -1120,12 +1141,13 @@ mod tests {
);
// Ensure updates to the file are reflected in the LSP.
buffer_1.update(cx, |buffer, cx| {
buffer_1.update(cx, |buffer, model, cx| {
buffer.file_updated(
Arc::new(File {
abs_path: "/root/child/buffer-1".into(),
path: Path::new("child/buffer-1").into(),
}),
model,
cx,
)
});
@@ -1155,7 +1177,7 @@ mod tests {
Ok(request::SignOutResult {})
});
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.update(cx, |copilot, model, cx| copilot.sign_out(model, cx))
.await
.unwrap();
assert_eq!(
@@ -1180,7 +1202,7 @@ mod tests {
})
});
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.update(cx, |copilot, model, cx| copilot.sign_in(model, cx))
.await
.unwrap();

View File

@@ -193,7 +193,7 @@ pub struct CopilotChat {
}
pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut AppContext) {
let copilot_chat = cx.new_model(|cx| CopilotChat::new(fs, client, cx));
let copilot_chat = cx.new_model(|model, cx| CopilotChat::new(fs, client, cx));
cx.set_global(GlobalCopilotChat(copilot_chat));
}
@@ -245,9 +245,9 @@ impl CopilotChat {
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.update(cx, |this, model, cx| {
this.oauth_token = oauth_token;
cx.notify();
model.notify(cx);
});
}
})?;
@@ -289,9 +289,9 @@ impl CopilotChat {
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
_ => {
let token = request_api_token(&oauth_token, client.clone()).await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.api_token = Some(token.clone());
cx.notify();
model.notify(cx);
})?;
token
}

View File

@@ -1,7 +1,7 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use gpui::{AppContext, EntityId, Model, Task};
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
@@ -85,10 +85,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
buffer: Model<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let copilot = self.copilot.clone();
self.pending_refresh = cx.spawn(|this, mut cx| async move {
self.pending_refresh = model.spawn(cx, |this, mut cx| async move {
if debounce {
cx.background_executor()
.timer(COPILOT_DEBOUNCE_TIMEOUT)
@@ -101,7 +102,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
})?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
if !completions.is_empty() {
this.cycled = false;
this.pending_cycling_refresh = Task::ready(Ok(()));
@@ -120,7 +121,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
for completion in completions {
this.push_completion(completion);
}
cx.notify();
model.notify(cx);
}
})?;
@@ -133,7 +134,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
buffer: Model<Buffer>,
cursor_position: language::Anchor,
direction: Direction,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if self.cycled {
match direction {
@@ -154,17 +156,17 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
}
}
cx.notify();
model.notify(cx);
} else {
let copilot = self.copilot.clone();
self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
self.pending_cycling_refresh = model.spawn(cx, |this, mut cx| async move {
let completions = copilot
.update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, cursor_position, cx)
})?
.await?;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.cycled = true;
this.file_extension = buffer.read(cx).file().and_then(|file| {
Some(
@@ -177,7 +179,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
for completion in completions {
this.push_completion(completion);
}
this.cycle(buffer, cursor_position, direction, cx);
this.cycle(buffer, cursor_position, direction, model, cx);
})?;
Ok(())
@@ -185,10 +187,12 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
}
}
fn accept(&mut self, cx: &mut ModelContext<Self>) {
fn accept(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(completion) = self.active_completion() {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.update(cx, |copilot, model, cx| {
copilot.accept_completion(completion, model, cx)
})
.detach_and_log_err(cx);
if self.active_completion().is_some() {
if let Some(telemetry) = self.telemetry.as_ref() {
@@ -205,7 +209,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
fn discard(
&mut self,
should_report_inline_completion_event: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let settings = AllLanguageSettings::get_global(cx);
@@ -216,8 +221,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
}
self.copilot
.update(cx, |copilot, cx| {
copilot.discard_completions(&self.completions, cx)
.update(cx, |copilot, model, cx| {
copilot.discard_completions(&self.completions, model, cx)
})
.detach_and_log_err(cx);
@@ -326,7 +331,7 @@ mod tests {
cx,
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
@@ -528,7 +533,7 @@ mod tests {
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, cx| {
editor.set_text("fn foo() {\n \n}", cx);
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
@@ -583,7 +588,7 @@ mod tests {
cx,
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
@@ -707,7 +712,7 @@ mod tests {
cx,
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
@@ -764,9 +769,9 @@ mod tests {
let (copilot, copilot_lsp) = Copilot::fake(cx);
let buffer_1 = cx.new_model(|cx| Buffer::local("a = 1\nb = 2\n", cx));
let buffer_2 = cx.new_model(|cx| Buffer::local("c = 3\nd = 4\n", cx));
let multibuffer = cx.new_model(|cx| {
let buffer_1 = cx.new_model(|model, cx| Buffer::local("a = 1\nb = 2\n", model, cx));
let buffer_2 = cx.new_model(|model, cx| Buffer::local("c = 3\nd = 4\n", model, cx));
let multibuffer = cx.new_model(|model, cx| {
let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
@@ -774,6 +779,7 @@ mod tests {
context: Point::new(0, 0)..Point::new(2, 0),
primary: None,
}],
model,
cx,
);
multibuffer.push_excerpts(
@@ -782,15 +788,19 @@ mod tests {
context: Point::new(0, 0)..Point::new(2, 0),
primary: None,
}],
model,
cx,
);
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let editor =
cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, model, cx));
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| editor.focus(window, cx))
.unwrap();
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, model, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
})
.unwrap();
@@ -804,15 +814,15 @@ mod tests {
}],
vec![],
);
_ = editor.update(cx, |editor, cx| {
_ = editor.update(cx, |editor, model, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.next_inline_completion(&Default::default(), cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
_ = editor.update(cx, |editor, model, cx| {
assert!(editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
@@ -830,9 +840,9 @@ mod tests {
}],
vec![],
);
_ = editor.update(cx, |editor, cx| {
_ = editor.update(cx, |editor, model, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion(cx));
@@ -854,7 +864,7 @@ mod tests {
// Ensure the new suggestion is displayed when the debounce timeout expires.
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
_ = editor.update(cx, |editor, model, cx| {
assert!(editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
@@ -883,7 +893,7 @@ mod tests {
cx,
)
.await;
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
@@ -1005,19 +1015,19 @@ mod tests {
let project = Project::test(fs, ["/test".as_ref()], cx).await;
let private_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/test/.env", cx)
.update(cx, |project, model, cx| {
project.open_local_buffer("/test/.env", model, cx)
})
.await
.unwrap();
let public_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/test/README.md", cx)
.update(cx, |project, model, cx| {
project.open_local_buffer("/test/README.md", model, cx)
})
.await
.unwrap();
let multibuffer = cx.new_model(|cx| {
let multibuffer = cx.new_model(|model, cx| {
let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite);
multibuffer.push_excerpts(
private_buffer.clone(),
@@ -1025,6 +1035,7 @@ mod tests {
context: Point::new(0, 0)..Point::new(1, 0),
primary: None,
}],
model,
cx,
);
multibuffer.push_excerpts(
@@ -1033,14 +1044,16 @@ mod tests {
context: Point::new(0, 0)..Point::new(1, 0),
primary: None,
}],
model,
cx,
);
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
let editor =
cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, model, cx));
let copilot_provider = cx.new_model(|_, _| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {
.update(cx, |editor, model, cx| {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
})
.unwrap();
@@ -1061,8 +1074,8 @@ mod tests {
},
);
_ = editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |selections| {
_ = editor.update(cx, |editor, model, cx| {
editor.change_selections(None, model, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.refresh_inline_completion(true, false, cx);
@@ -1071,8 +1084,8 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
assert!(copilot_requests.try_next().is_err());
_ = editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
_ = editor.update(cx, |editor, model, cx| {
editor.change_selections(None, model, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
});
editor.refresh_inline_completion(true, false, cx);

View File

@@ -2,7 +2,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
div, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Render,
Styled, Subscription, ViewContext,
Styled, Subscription,
};
use ui::{prelude::*, Button, Label, Vector, VectorName};
use util::ResultExt as _;
@@ -13,7 +13,7 @@ const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
struct CopilotStartingToast;
pub fn initiate_sign_in(cx: &mut WindowContext) {
pub fn initiate_sign_in(window: &mut gpui::Window, cx: &mut gpui::AppContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
@@ -27,7 +27,7 @@ pub fn initiate_sign_in(cx: &mut WindowContext) {
return;
};
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
let Ok(workspace) = workspace.update(cx, |workspace, model, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
@@ -58,7 +58,7 @@ pub fn initiate_sign_in(cx: &mut WindowContext) {
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.update(cx, |copilot, model, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
@@ -68,10 +68,12 @@ pub fn initiate_sign_in(cx: &mut WindowContext) {
.detach();
}
_ => {
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
copilot
.update(cx, |this, model, cx| this.sign_in(cx))
.detach();
workspace
.update(cx, |this, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
.update(cx, |this, model, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, model, cx));
})
.ok();
}
@@ -95,32 +97,33 @@ impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
impl ModalView for CopilotCodeVerification {}
impl CopilotCodeVerification {
pub fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(copilot: &Model<Copilot>, model: &Model<Self>, cx: &mut AppContext) -> Self {
let status = copilot.read(cx).status();
Self {
status,
connect_clicked: false,
focus_handle: cx.focus_handle(),
focus_handle: window.focus_handle(),
_subscription: cx.observe(copilot, |this, copilot, cx| {
let status = copilot.read(cx).status();
match status {
Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => {
this.set_status(status, cx)
}
_ => cx.emit(DismissEvent),
_ => model.emit(DismissEvent, cx),
}
}),
}
}
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
pub fn set_status(&mut self, status: Status, model: &Model<Self>, cx: &mut AppContext) {
self.status = status;
cx.notify();
model.notify(cx);
}
fn render_device_code(
data: &PromptUserDeviceFlow,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl IntoElement {
let copied = cx
.read_from_clipboard()
@@ -152,7 +155,8 @@ impl CopilotCodeVerification {
fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> impl Element {
let connect_button_label = if connect_clicked {
"Waiting for connection..."
@@ -168,7 +172,7 @@ impl CopilotCodeVerification {
Label::new("Using Copilot requires an active subscription on GitHub.")
.color(Color::Muted),
)
.child(Self::render_device_code(data, cx))
.child(Self::render_device_code(data, model, cx))
.child(
Label::new("Paste this code into GitHub after clicking the button below.")
.size(ui::LabelSize::Small),
@@ -177,7 +181,7 @@ impl CopilotCodeVerification {
Button::new("connect-button", connect_button_label)
.on_click({
let verification_uri = data.verification_uri.clone();
cx.listener(move |this, _, cx| {
model.listener(move |this, _, cx| {
cx.open_url(&verification_uri);
this.connect_clicked = true;
})
@@ -188,10 +192,10 @@ impl CopilotCodeVerification {
.child(
Button::new("copilot-enable-cancel-button", "Cancel")
.full_width()
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
.on_click(cx.listener(|_, _, cx| model.emit(DismissEvent, cx))),
)
}
fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
fn render_enabled_modal(model: &Model<Self>, cx: &mut AppContext) -> impl Element {
v_flex()
.gap_2()
.child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
@@ -201,11 +205,11 @@ impl CopilotCodeVerification {
.child(
Button::new("copilot-enabled-done-button", "Done")
.full_width()
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
.on_click(cx.listener(|_, _, cx| model.emit(DismissEvent, cx))),
)
}
fn render_unauthorized_modal(cx: &mut ViewContext<Self>) -> impl Element {
fn render_unauthorized_modal(model: &Model<Self>, cx: &mut AppContext) -> impl Element {
v_flex()
.child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
@@ -220,7 +224,7 @@ impl CopilotCodeVerification {
.child(
Button::new("copilot-subscribe-cancel-button", "Cancel")
.full_width()
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
.on_click(cx.listener(|_, _, cx| model.emit(DismissEvent, cx))),
)
}
@@ -232,18 +236,24 @@ impl CopilotCodeVerification {
}
impl Render for CopilotCodeVerification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let prompt = match &self.status {
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
} => Self::render_prompting_modal(self.connect_clicked, prompt, model, cx)
.into_any_element(),
Status::Unauthorized => {
self.connect_clicked = false;
Self::render_unauthorized_modal(cx).into_any_element()
Self::render_unauthorized_modal(model, cx).into_any_element()
}
Status::Authorized => {
self.connect_clicked = false;
Self::render_enabled_modal(cx).into_any_element()
Self::render_enabled_modal(model, cx).into_any_element()
}
Status::Disabled => {
self.connect_clicked = false;
@@ -261,7 +271,7 @@ impl Render for CopilotCodeVerification {
.p_4()
.gap_2()
.on_action(cx.listener(|_, _: &menu::Cancel, cx| {
cx.emit(DismissEvent);
model.emit(DismissEvent, cx);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, cx| {
cx.focus(&this.focus_handle);

View File

@@ -15,13 +15,14 @@ use editor::{
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
Render, SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
WeakView, WindowContext,
actions, div, svg, AnyElement, AnyView, AppContext, AppContext, Context, EventEmitter,
FocusHandle, FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model,
ParentElement, Render, SharedString, Styled, StyledText, Subscription, Task, View,
VisualContext, WeakView,
};
use language::{
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, Point,
Selection, SelectionGoal,
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
@@ -57,9 +58,9 @@ pub fn init(cx: &mut AppContext) {
struct ProjectDiagnosticsEditor {
project: Model<Project>,
workspace: WeakView<Workspace>,
workspace: WeakModel<Workspace>,
focus_handle: FocusHandle,
editor: View<Editor>,
editor: Model<Editor>,
summary: DiagnosticSummary,
excerpts: Model<MultiBuffer>,
path_states: Vec<PathState>,
@@ -89,7 +90,12 @@ impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let child = if self.path_states.is_empty() {
div()
.bg(cx.theme().colors().editor_background)
@@ -114,21 +120,22 @@ impl Render for ProjectDiagnosticsEditor {
}
impl ProjectDiagnosticsEditor {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(Self::deploy);
fn register(workspace: &mut Workspace, model: &Model<Workspace>, _: &mut AppContext) {
workspace.register_action(model, Self::deploy);
}
fn new_with_context(
context: u32,
include_warnings: bool,
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
workspace: WeakModel<Workspace>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let project_event_subscription =
cx.subscribe(&project_handle, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
model.notify(cx);
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}");
@@ -149,7 +156,7 @@ impl ProjectDiagnosticsEditor {
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
model.emit(EditorEvent::TitleChanged, cx);
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
@@ -164,21 +171,27 @@ impl ProjectDiagnosticsEditor {
_ => {}
});
let focus_handle = cx.focus_handle();
let focus_handle = window.focus_handle();
cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
.detach();
cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
.detach();
let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
editor.set_vertical_scroll_margin(5, cx);
let excerpts =
cx.new_model(|model, cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new_model(|model, cx| {
let mut editor = Editor::for_multibuffer(
excerpts.clone(),
Some(project_handle.clone()),
false,
model,
cx,
);
editor.set_vertical_scroll_margin(5, model, cx);
editor
});
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
cx.emit(event.clone());
model.emit(event.clone(), cx);
match event {
EditorEvent::Focused => {
if this.path_states.is_empty() {
@@ -211,21 +224,21 @@ impl ProjectDiagnosticsEditor {
update_excerpts_task: None,
_subscription: project_event_subscription,
};
this.update_all_excerpts(cx);
this.update_all_excerpts(model, cx);
this
}
fn update_stale_excerpts(&mut self, cx: &mut ViewContext<Self>) {
fn update_stale_excerpts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.update_excerpts_task.is_some() {
return;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move {
self.update_excerpts_task = Some(model.spawn(cx, |this, mut cx| async move {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
.await;
loop {
let Some((path, language_server_id)) = this.update(&mut cx, |this, _| {
let Some((path, language_server_id)) = this.update(&mut cx, |this, _, _| {
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
this.update_excerpts_task.take();
return None;
@@ -241,8 +254,8 @@ impl ProjectDiagnosticsEditor {
.await
.log_err()
{
this.update(&mut cx, |this, cx| {
this.update_excerpts(path, language_server_id, buffer, cx);
this.update(&mut cx, |this, model, cx| {
this.update_excerpts(path, language_server_id, buffer, model, cx);
})?;
}
}
@@ -253,64 +266,73 @@ impl ProjectDiagnosticsEditor {
fn new(
project_handle: Model<Project>,
include_warnings: bool,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
workspace: WeakModel<Workspace>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
Self::new_with_context(
editor::DEFAULT_MULTIBUFFER_CONTEXT,
include_warnings,
project_handle,
workspace,
model,
cx,
)
}
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
fn deploy(
workspace: &mut Workspace,
_: &Deploy,
model: &Model<Workspace>,
cx: &mut AppContext,
) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
workspace.activate_item(&existing, true, true, cx);
workspace.activate_item(&existing, true, true, model, cx);
} else {
let workspace_handle = cx.view().downgrade();
let workspace_handle = model.downgrade();
let include_warnings = match cx.try_global::<IncludeWarnings>() {
Some(include_warnings) => include_warnings.0,
None => ProjectDiagnosticsSettings::get_global(cx).include_warnings,
};
let diagnostics = cx.new_view(|cx| {
let diagnostics = cx.new_model(|model, cx| {
ProjectDiagnosticsEditor::new(
workspace.project().clone(),
include_warnings,
workspace_handle,
model,
cx,
)
});
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, model, cx);
}
}
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
fn toggle_warnings(&mut self, _: &ToggleWarnings, model: &Model<Self>, cx: &mut AppContext) {
self.include_warnings = !self.include_warnings;
cx.set_global(IncludeWarnings(self.include_warnings));
self.update_all_excerpts(cx);
cx.notify();
self.update_all_excerpts(model, cx);
model.notify(cx);
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
self.editor.focus_handle(cx).focus(cx)
fn focus_in(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.focus_handle.is_focused(window) && !self.path_states.is_empty() {
self.editor.item_focus_handle(cx).focus(window)
}
}
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
self.update_stale_excerpts(cx);
fn focus_out(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if !self.focus_handle.is_focused(window) && !self.editor.item_focus_handle(cx).is_focused(window)
{
self.update_stale_excerpts(model, cx);
}
}
/// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
fn update_all_excerpts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.project.update(cx, |project, model, cx| {
let mut paths = project
.diagnostic_summaries(false, cx)
.map(|(path, _, _)| (path, None))
@@ -324,7 +346,7 @@ impl ProjectDiagnosticsEditor {
paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
self.paths_to_update = paths;
});
self.update_stale_excerpts(cx);
self.update_stale_excerpts(model, cx);
}
fn update_excerpts(
@@ -332,7 +354,8 @@ impl ProjectDiagnosticsEditor {
path_to_update: ProjectPath,
server_to_update: Option<LanguageServerId>,
buffer: Model<Buffer>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let was_empty = self.path_states.is_empty();
let snapshot = buffer.read(cx).snapshot();
@@ -369,7 +392,7 @@ impl ProjectDiagnosticsEditor {
let mut blocks_to_add = Vec::new();
let mut blocks_to_remove = HashSet::default();
let mut first_excerpt_id = None;
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, model, cx| {
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
.into_iter()
.enumerate()
@@ -440,13 +463,20 @@ impl ProjectDiagnosticsEditor {
}
}
let excerpt_start =
let mut excerpt_start =
Point::new(range.start.row.saturating_sub(self.context), 0);
let excerpt_end = snapshot.clip_point(
let mut excerpt_end = snapshot.clip_point(
Point::new(range.end.row + self.context, u32::MAX),
Bias::Left,
);
if let Some(ancestor_range) = snapshot.range_for_syntax_ancestor(range)
{
let ancestor_range = ancestor_range.to_point(&snapshot);
excerpt_start = excerpt_start.min(ancestor_range.start);
excerpt_end = excerpt_end.max(ancestor_range.end);
}
let excerpt_id = excerpts
.insert_excerpts_after(
prev_excerpt_id,
@@ -455,6 +485,7 @@ impl ProjectDiagnosticsEditor {
context: excerpt_start..excerpt_end,
primary: Some(range.clone()),
}],
model,
cx,
)
.pop()
@@ -517,7 +548,7 @@ impl ProjectDiagnosticsEditor {
new_group_ixs.push(path_state.diagnostic_groups.len());
path_state.diagnostic_groups.push(group_state);
} else if let Some((_, group_state)) = to_remove {
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx);
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), model, cx);
blocks_to_remove.extend(group_state.blocks.iter().copied());
} else if let Some((_, group_state)) = to_keep {
prev_excerpt_id = *group_state.excerpts.last().unwrap();
@@ -529,8 +560,8 @@ impl ProjectDiagnosticsEditor {
excerpts.snapshot(cx)
});
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
self.editor.update(cx, |editor, model, cx| {
editor.remove_blocks(blocks_to_remove, None, model, cx);
let block_ids = editor.insert_blocks(
blocks_to_add.into_iter().flat_map(|block| {
let placement = match block.placement {
@@ -555,6 +586,7 @@ impl ProjectDiagnosticsEditor {
})
}),
Some(Autoscroll::fit()),
model,
cx,
);
@@ -569,7 +601,7 @@ impl ProjectDiagnosticsEditor {
self.path_states.remove(path_ix);
}
self.editor.update(cx, |editor, cx| {
self.editor.update(cx, |editor, model, cx| {
let groups;
let mut selections;
let new_excerpt_ids_by_selection_id;
@@ -586,12 +618,12 @@ impl ProjectDiagnosticsEditor {
} else {
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
new_excerpt_ids_by_selection_id =
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
editor.change_selections(Some(Autoscroll::fit()), model, cx, |s| s.refresh());
selections = editor.selections.all::<usize>(cx);
}
// If any selection has lost its position, move it to start of the next primary diagnostic.
let snapshot = editor.snapshot(cx);
let snapshot = editor.snapshot(model, cx);
for selection in &mut selections {
if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
let group_ix = match groups.binary_search_by(|probe| {
@@ -617,29 +649,29 @@ impl ProjectDiagnosticsEditor {
}
}
}
editor.change_selections(None, cx, |s| {
editor.change_selections(None, model, cx, |s| {
s.select(selections);
});
Some(())
});
if self.path_states.is_empty() {
if self.editor.focus_handle(cx).is_focused(cx) {
if self.editor.item_focus_handle(cx).is_focused(window) {
cx.focus(&self.focus_handle);
}
} else if self.focus_handle.is_focused(cx) {
let focus_handle = self.editor.focus_handle(cx);
} else if self.focus_handle.is_focused(window) {
let focus_handle = self.editor.item_focus_handle(cx);
cx.focus(&focus_handle);
}
#[cfg(test)]
self.check_invariants(cx);
self.check_invariants(model, cx);
cx.notify();
model.notify(cx);
}
#[cfg(test)]
fn check_invariants(&self, cx: &mut ViewContext<Self>) {
fn check_invariants(&self, model: &Model<Self>, cx: &mut AppContext) {
let mut excerpts = Vec::new();
for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
if let Some(file) = buffer.file() {
@@ -680,20 +712,21 @@ impl Item for ProjectDiagnosticsEditor {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
fn deactivated(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.editor
.update(cx, |editor, model, cx| editor.deactivated(model, cx));
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
fn navigate(&mut self, data: Box<dyn Any>, model: &Model<Self>, cx: &mut AppContext) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))
.update(cx, |editor, model, cx| editor.navigate(data, model, cx))
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
Some("Project Diagnostics".into())
}
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
fn tab_content(&self, params: TabContentParams, _: &Window, _: &AppContext) -> AnyElement {
h_flex()
.gap_1()
.when(
@@ -748,8 +781,13 @@ impl Item for ProjectDiagnosticsEditor {
false
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, _| {
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.editor.update(cx, |editor, model, _| {
editor.set_nav_history(Some(nav_history));
});
}
@@ -757,16 +795,18 @@ impl Item for ProjectDiagnosticsEditor {
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<Model<Self>>
where
Self: Sized,
{
Some(cx.new_view(|cx| {
Some(cx.new_model(|model, cx| {
ProjectDiagnosticsEditor::new(
self.project.clone(),
self.include_warnings,
self.workspace.clone(),
model,
cx,
)
}))
@@ -792,7 +832,8 @@ impl Item for ProjectDiagnosticsEditor {
&mut self,
format: bool,
project: Model<Project>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.editor.save(format, project, cx)
}
@@ -801,25 +842,31 @@ impl Item for ProjectDiagnosticsEditor {
&mut self,
_: Model<Project>,
_: ProjectPath,
_: &mut ViewContext<Self>,
_: &Model<Self>,
_: &mut AppContext,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
fn reload(
&mut self,
project: Model<Project>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Task<Result<()>> {
self.editor.reload(project, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a View<Self>,
self_handle: &'a Model<Self>,
_: &'a AppContext,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.model())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.model())
} else {
None
}
@@ -833,9 +880,15 @@ impl Item for ProjectDiagnosticsEditor {
self.editor.breadcrumbs(theme, cx)
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
model: &Model<Self>,
cx: &mut AppContext,
) {
self.editor.update(cx, |editor, model, cx| {
editor.added_to_workspace(workspace, model, cx)
});
}
}
@@ -859,27 +912,24 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
h_flex()
.gap_3()
.map(|stack| {
stack.child(
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
}),
)
stack.child(svg().size(window.text_style().font_size).flex_none().map(
|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::Warning.path())
.text_color(Color::Warning.color(cx))
}
},
))
})
.child(
h_flex()
.gap_1()
.child(
StyledText::new(message.clone()).with_highlights(
&cx.text_style(),
&window.text_style(),
code_ranges
.iter()
.map(|range| (range.clone(), highlight_style)),

View File

@@ -61,12 +61,12 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), model, cx));
let cx = &mut VisualTestContext::from_window(*window, model, cx);
let workspace = window.root(cx).unwrap();
// Create some diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store
.update_diagnostic_entries(
language_server_id,
@@ -159,7 +159,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
cx,
)
});
let editor = view.update(cx, |view, _| view.editor.clone());
let editor = view.update(cx, |view, model, _| view.editor.clone());
view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.await;
@@ -174,7 +174,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
]
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
//
// main.rs
@@ -215,7 +215,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
);
// Cursor is at the first diagnostic
editor.update(cx, |editor, cx| {
editor.update(cx, |editor, model, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
@@ -223,7 +223,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
// Diagnostics are added for another earlier path.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
.update_diagnostic_entries(
@@ -263,7 +263,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
//
// consts.rs
@@ -315,7 +315,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
);
// Cursor keeps its position.
editor.update(cx, |editor, cx| {
editor.update(cx, |editor, model, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
@@ -323,7 +323,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
// Diagnostics are added to the first path
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_started(language_server_id, cx);
lsp_store
.update_diagnostic_entries(
@@ -378,7 +378,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
//
// consts.rs
@@ -460,8 +460,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), model, cx));
let cx = &mut VisualTestContext::from_window(*window, model, cx);
let workspace = window.root(cx).unwrap();
let view = window.build_view(cx, |cx| {
@@ -473,10 +473,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
cx,
)
});
let editor = view.update(cx, |view, _| view.editor.clone());
let editor = view.update(cx, |view, model, _| view.editor.clone());
// Two language servers start updating diagnostics
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
@@ -501,7 +501,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
});
// The first language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
});
@@ -517,7 +517,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
]
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
@@ -530,7 +530,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// The second language server finishes
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store
.update_diagnostic_entries(
server_id_2,
@@ -567,7 +567,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
]
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
@@ -587,7 +587,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// Both language servers start updating diagnostics, and the first server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_started(server_id_1, cx);
lsp_store.disk_based_diagnostics_started(server_id_2, cx);
lsp_store
@@ -635,7 +635,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
]
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
@@ -656,7 +656,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
);
// The second language server finishes.
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store
.update_diagnostic_entries(
server_id_2,
@@ -693,7 +693,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
]
);
assert_eq!(
editor.update(cx, |editor, cx| editor.display_text(cx)),
editor.update(cx, |editor, model, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
@@ -727,8 +727,8 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), model, cx));
let cx = &mut VisualTestContext::from_window(*window, model, cx);
let workspace = window.root(cx).unwrap();
let mutated_view = window.build_view(cx, |cx| {
@@ -741,11 +741,11 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
)
});
workspace.update(cx, |workspace, cx| {
workspace.update(cx, |workspace, model, cx| {
workspace.add_item_to_center(Box::new(mutated_view.clone()), cx);
});
mutated_view.update(cx, |view, cx| {
assert!(view.focus_handle.is_focused(cx));
mutated_view.update(cx, |view, model, cx| {
assert!(view.focus_handle.is_focused(window));
});
let mut next_group_id = 0;
@@ -763,7 +763,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update(cx, |lsp_store, model, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
@@ -809,7 +809,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
updated_language_servers.insert(server_id);
project.update(cx, |project, cx| {
project.update(cx, |project, model, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
@@ -819,7 +819,14 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
&mut rng,
);
project
.update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
.update_diagnostic_entries(
server_id,
path,
None,
diagnostics.clone(),
model,
cx,
)
.unwrap()
});
@@ -829,7 +836,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
}
log::info!("updating mutated diagnostics view");
mutated_view.update(cx, |view, cx| view.update_stale_excerpts(cx));
mutated_view.update(cx, |view, model, cx| view.update_stale_excerpts(cx));
cx.run_until_parked();
log::info!("constructing reference diagnostics view");
@@ -873,13 +880,13 @@ struct ExcerptInfo {
}
fn get_diagnostics_excerpts(
view: &View<ProjectDiagnosticsEditor>,
view: &Model<ProjectDiagnosticsEditor>,
cx: &mut VisualTestContext,
) -> Vec<ExcerptInfo> {
view.update(cx, |view, cx| {
view.update(cx, |view, model, cx| {
let mut result = vec![];
let mut excerpt_indices_by_id = HashMap::default();
view.excerpts.update(cx, |multibuffer, cx| {
view.excerpts.update(cx, |multibuffer, model, cx| {
let snapshot = multibuffer.snapshot(cx);
for (id, buffer, range) in snapshot.excerpts() {
excerpt_indices_by_id.insert(id, result.len());
@@ -999,13 +1006,13 @@ const FILE_HEADER: &str = "file header";
const EXCERPT_HEADER: &str = "excerpt header";
fn editor_blocks(
editor: &View<Editor>,
editor: &Model<Editor>,
cx: &mut VisualTestContext,
) -> Vec<(DisplayRow, SharedString)> {
let mut blocks = Vec::new();
cx.draw(gpui::Point::default(), AvailableSpace::min_size(), |cx| {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
editor.update(cx, |editor, model, cx| {
let snapshot = editor.snapshot(model, cx);
blocks.extend(
snapshot
.blocks_in_range(DisplayRow(0)..snapshot.max_point().row())

View File

@@ -2,8 +2,8 @@ use std::time::Duration;
use editor::Editor;
use gpui::{
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
ViewContext, WeakView,
AppContext, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
WeakView,
};
use language::Diagnostic;
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
@@ -13,15 +13,20 @@ use crate::{Deploy, ProjectDiagnosticsEditor};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
active_editor: Option<WeakModel<Editor>>,
workspace: WeakModel<Workspace>,
current_diagnostic: Option<Diagnostic>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
}
impl Render for DiagnosticIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_flex().map(|this| {
this.child(
@@ -67,10 +72,15 @@ impl Render for DiagnosticIndicator {
Some(
Button::new("diagnostic_message", message)
.label_size(LabelSize::Small)
.tooltip(|cx| {
Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx)
.tooltip(|window, cx| {
Tooltip::for_action(
"Next Diagnostic",
&editor::actions::GoToDiagnostic,
model,
cx,
)
})
.on_click(cx.listener(|this, _, cx| {
.on_click(model.listener(|this, model, _, cx| {
this.go_to_next_diagnostic(cx);
}))
.into_any_element(),
@@ -87,11 +97,18 @@ impl Render for DiagnosticIndicator {
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)
.tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
.on_click(cx.listener(|this, _, cx| {
.tooltip(|window, cx| {
Tooltip::for_action("Project Diagnostics", &Deploy, model, cx)
})
.on_click(model.listener(|this, model, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
workspace.update(cx, |workspace, model, cx| {
ProjectDiagnosticsEditor::deploy(
workspace,
&Default::default(),
model,
cx,
)
})
}
})),
@@ -101,22 +118,22 @@ impl Render for DiagnosticIndicator {
}
impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
pub fn new(workspace: &Workspace, model: &Model<Self>, cx: &mut AppContext) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
model.notify(cx);
}
project::Event::DiskBasedDiagnosticsFinished { .. }
| project::Event::LanguageServerRemoved(_) => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
model.notify(cx);
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.notify();
model.notify(cx);
}
_ => {}
@@ -133,16 +150,16 @@ impl DiagnosticIndicator {
}
}
fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
fn go_to_next_diagnostic(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
editor.update(cx, |editor, cx| {
editor.update(cx, |editor, model, cx| {
editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
})
}
}
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
fn update(&mut self, editor: Model<Editor>, model: &Model<Self>, cx: &mut AppContext) {
let (buffer, cursor_position) = editor.update(cx, |editor, model, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let cursor_position = editor.selections.newest::<usize>(cx).head();
(buffer, cursor_position)
@@ -160,7 +177,7 @@ impl DiagnosticIndicator {
diagnostics_indicator
.update(&mut cx, |diagnostics_indicator, cx| {
diagnostics_indicator.current_diagnostic = new_diagnostic;
cx.notify();
model.notify(cx);
})
.ok();
});
@@ -174,17 +191,18 @@ impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_editor = Some(editor.downgrade());
self._observe_active_editor = Some(cx.observe(&editor, Self::update));
self.update(editor, cx);
self.update(editor, model, cx);
} else {
self.active_editor = None;
self.current_diagnostic = None;
self._observe_active_editor = None;
}
cx.notify();
model.notify(cx);
}
}

View File

@@ -1,15 +1,20 @@
use crate::ProjectDiagnosticsEditor;
use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView};
use gpui::{AppContext, EventEmitter, ParentElement, Render, View, WeakView};
use ui::prelude::*;
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub struct ToolbarControls {
editor: Option<WeakView<ProjectDiagnosticsEditor>>,
editor: Option<WeakModel<ProjectDiagnosticsEditor>>,
}
impl Render for ToolbarControls {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
@@ -47,10 +52,10 @@ impl Render for ToolbarControls {
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.disabled(is_updating)
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
.on_click(cx.listener(|this, _, cx| {
.tooltip(move |window, cx| Tooltip::text("Update excerpts", cx))
.on_click(model.listener(|this, model, _, cx| {
if let Some(diagnostics) = this.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.update(cx, |diagnostics, model, cx| {
diagnostics.update_all_excerpts(cx);
});
}
@@ -61,10 +66,10 @@ impl Render for ToolbarControls {
IconButton::new("toggle-warnings", IconName::Warning)
.icon_color(warning_color)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
.tooltip(move |window, cx| Tooltip::text(tooltip, cx))
.on_click(model.listener(|this, model, _, cx| {
if let Some(editor) = this.diagnostics() {
editor.update(cx, |editor, cx| {
editor.update(cx, |editor, model, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
@@ -79,7 +84,8 @@ impl ToolbarItemView for ToolbarControls {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut ViewContext<Self>,
_: &Model<Self>,
_: &mut AppContext,
) -> ToolbarItemLocation {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
@@ -105,7 +111,7 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
fn diagnostics(&self) -> Option<View<ProjectDiagnosticsEditor>> {
fn diagnostics(&self) -> Option<Model<ProjectDiagnosticsEditor>> {
self.editor.as_ref()?.upgrade()
}
}

View File

@@ -27,7 +27,11 @@ impl<'a> CommitAvatar<'a> {
}
impl<'a> CommitAvatar<'a> {
fn render(&'a self, cx: &mut ViewContext<BlameEntryTooltip>) -> Option<impl IntoElement> {
fn render(
&'a self,
model: &Model<BlameEntryTooltip>,
cx: &mut AppContext,
) -> Option<impl IntoElement> {
let remote = self
.details
.and_then(|details| details.remote.as_ref())
@@ -91,7 +95,7 @@ pub(crate) struct BlameEntryTooltip {
blame_entry: BlameEntry,
details: Option<CommitDetails>,
editor_style: EditorStyle,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
scroll_handle: ScrollHandle,
}
@@ -100,7 +104,7 @@ impl BlameEntryTooltip {
blame_entry: BlameEntry,
details: Option<CommitDetails>,
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
workspace: Option<WeakModel<Workspace>>,
) -> Self {
Self {
editor_style: style.clone(),
@@ -113,8 +117,14 @@ impl BlameEntryTooltip {
}
impl Render for BlameEntryTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let avatar = CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha).render(cx);
fn render(
&mut self,
model: &Model<Self>,
window: &mut gpui::Window,
cx: &mut AppContext,
) -> impl IntoElement {
let avatar = CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha)
.render(model, window, cx);
let author = self
.blame_entry

View File

@@ -15,7 +15,7 @@ pub struct BlinkManager {
}
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
pub fn new(blink_interval: Duration, model: &Model<Self>, cx: &mut AppContext) -> Self {
// Make sure we blink the cursors if the setting is re-enabled
cx.observe_global::<SettingsStore>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx)
@@ -37,55 +37,59 @@ impl BlinkManager {
self.blink_epoch
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
pub fn pause_blinking(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.show_cursor(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| async move {
Timer::after(interval).await;
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
})
.detach();
model
.spawn(cx, |this, mut cx| async move {
Timer::after(interval).await;
this.update(&mut cx, |this, model, cx| {
this.resume_cursor_blinking(epoch, cx)
})
})
.detach();
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
fn resume_cursor_blinking(&mut self, epoch: usize, model: &Model<Self>, cx: &mut AppContext) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
self.blink_cursors(epoch, model, cx);
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
fn blink_cursors(&mut self, epoch: usize, model: &Model<Self>, cx: &mut AppContext) {
if EditorSettings::get_global(cx).cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();
model.notify(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
.ok();
}
})
.detach();
model
.spawn(cx, |this, mut cx| async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, model, cx| this.blink_cursors(epoch, cx))
.ok();
}
})
.detach();
}
} else {
self.show_cursor(cx);
}
}
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
pub fn show_cursor(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if !self.visible {
self.visible = true;
cx.notify();
model.notify(cx);
}
}
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
pub fn enable(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if self.enabled {
return;
}
@@ -94,10 +98,10 @@ impl BlinkManager {
// Set cursors as invisible and start blinking: this causes cursors
// to be visible during the next render.
self.visible = false;
self.blink_cursors(self.blink_epoch, cx);
self.blink_cursors(self.blink_epoch, model, cx);
}
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
pub fn disable(&mut self, model: &Model<Self>, _cx: &mut AppContext) {
self.visible = false;
self.enabled = false;
}

View File

@@ -1,5 +1,5 @@
use anyhow::Context as _;
use gpui::{View, ViewContext, WindowContext};
use gpui::{AppContext, Model};
use language::Language;
use url::Url;
@@ -16,7 +16,8 @@ fn is_c_language(language: &Language) -> bool {
pub fn switch_source_header(
editor: &mut Editor,
_: &SwitchSourceHeader,
cx: &mut ViewContext<'_, Editor>,
model: &Model<Editor>,
cx: &mut AppContext,
) {
let Some(project) = &editor.project else {
return;
@@ -41,7 +42,7 @@ pub fn switch_source_header(
.unwrap()
.to_owned();
let switch_source_header_task = project.update(cx, |project, cx| {
let switch_source_header_task = project.update(cx, |project, model, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
@@ -84,8 +85,12 @@ pub fn switch_source_header(
.detach_and_log_err(cx);
}
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
if editor.update(cx, |e, cx| {
pub fn apply_related_actions(
editor: &Model<Editor>,
window: &mut gpui::Window,
cx: &mut gpui::AppContext,
) {
if editor.update(cx, |e, model, cx| {
find_specific_language_server_in_selection(e, cx, is_c_language, CLANGD_SERVER_NAME)
.is_some()
}) {

View File

@@ -1,7 +1,7 @@
use std::time::Duration;
use futures::{channel::oneshot, FutureExt};
use gpui::{Task, ViewContext};
use gpui::{AppContext, Model, Task};
use crate::Editor;
@@ -19,9 +19,14 @@ impl DebouncedDelay {
}
}
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Editor>, func: F)
where
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
pub fn fire_new<F>(
&mut self,
delay: Duration,
model: &Model<Editor>,
cx: &mut AppContext,
func: F,
) where
F: 'static + Send + FnOnce(&mut Editor, &Model<Editor>, &mut AppContext) -> Task<()>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());

View File

@@ -37,9 +37,7 @@ use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
use gpui::{AnyElement, Font, HighlightStyle, LineLayout, Model, Pixels, UnderlineStyle};
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
@@ -66,7 +64,7 @@ use std::{
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use ui::{px, SharedString, WindowContext};
use ui::{px, SharedString};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -76,7 +74,8 @@ pub enum FoldStatus {
Foldable,
}
pub type RenderFoldToggle = Arc<dyn Fn(FoldStatus, &mut WindowContext) -> AnyElement>;
pub type RenderFoldToggle =
Arc<dyn Fn(FoldStatus, &mut gpui::Window, &mut gpui::AppContext) -> AnyElement>;
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
@@ -126,11 +125,12 @@ impl DisplayMap {
excerpt_header_height: u32,
excerpt_footer_height: u32,
fold_placeholder: FoldPlaceholder,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let tab_size = Self::tab_size(&buffer, cx);
let tab_size = Self::tab_size(&buffer, model, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let crease_map = CreaseMap::new(&buffer_snapshot);
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
@@ -145,7 +145,7 @@ impl DisplayMap {
excerpt_footer_height,
);
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
cx.observe(&wrap_map, |_, _, cx| model.notify(cx)).detach();
DisplayMap {
buffer,
@@ -164,16 +164,16 @@ impl DisplayMap {
}
}
pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
pub fn snapshot(&mut self, model: &Model<Self>, cx: &mut AppContext) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let (wrap_snapshot, edits) = self.wrap_map.update(cx, |map, model, cx| {
map.sync(tab_snapshot.clone(), edits, model, cx)
});
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
DisplaySnapshot {
@@ -192,7 +192,7 @@ impl DisplayMap {
}
}
pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext<Self>) {
pub fn set_state(&mut self, other: &DisplaySnapshot, model: &Model<Self>, cx: &mut AppContext) {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
@@ -203,6 +203,7 @@ impl DisplayMap {
)
})
.collect(),
model,
cx,
);
}
@@ -211,17 +212,18 @@ impl DisplayMap {
pub fn fold<T: Clone + ToOffset>(
&mut self,
creases: Vec<Crease<T>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.read(snapshot, edits);
let inline = creases.iter().filter_map(|crease| {
@@ -239,7 +241,7 @@ impl DisplayMap {
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let mut block_map = self.block_map.write(snapshot, edits);
let blocks = creases.into_iter().filter_map(|crease| {
if let Crease::Block {
@@ -284,23 +286,24 @@ impl DisplayMap {
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
type_id: TypeId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.remove_folds(ranges, type_id);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.write(snapshot, edits);
}
@@ -309,7 +312,8 @@ impl DisplayMap {
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let offset_ranges = ranges
@@ -317,13 +321,13 @@ impl DisplayMap {
.map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot))
.collect::<Vec<_>>();
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) =
@@ -331,7 +335,7 @@ impl DisplayMap {
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
@@ -339,7 +343,8 @@ impl DisplayMap {
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease<Anchor>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Vec<CreaseId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.crease_map.insert(creases, &snapshot)
@@ -348,7 +353,8 @@ impl DisplayMap {
pub fn remove_creases(
&mut self,
crease_ids: impl IntoIterator<Item = CreaseId>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.crease_map.remove(crease_ids, &snapshot)
@@ -357,17 +363,18 @@ impl DisplayMap {
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Vec<CustomBlockId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.insert(blocks)
}
@@ -375,17 +382,18 @@ impl DisplayMap {
pub fn resize_blocks(
&mut self,
heights: HashMap<CustomBlockId, u32>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.resize(heights);
}
@@ -394,16 +402,21 @@ impl DisplayMap {
self.block_map.replace_blocks(renderers);
}
pub fn remove_blocks(&mut self, ids: HashSet<CustomBlockId>, cx: &mut ModelContext<Self>) {
pub fn remove_blocks(
&mut self,
ids: HashSet<CustomBlockId>,
model: &Model<Self>,
cx: &mut AppContext,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.remove(ids);
}
@@ -411,17 +424,18 @@ impl DisplayMap {
pub fn row_for_block(
&mut self,
block_id: CustomBlockId,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> Option<DisplayRow> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
let block_map = self.block_map.read(snapshot, edits);
let block_row = block_map.row_for_block(block_id)?;
Some(DisplayRow(block_row.0))
@@ -466,14 +480,26 @@ impl DisplayMap {
cleared
}
pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_font_with_size(font, font_size, cx))
pub fn set_font(
&self,
font: Font,
font_size: Pixels,
model: &Model<Self>,
cx: &mut AppContext,
) -> bool {
self.wrap_map.update(cx, |map, model, cx| {
map.set_font_with_size(font, font_size, model, cx)
})
}
pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut ModelContext<Self>) -> bool {
pub fn set_wrap_width(
&self,
width: Option<Pixels>,
model: &Model<Self>,
cx: &mut AppContext,
) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
.update(cx, |map, model, cx| map.set_wrap_width(width, model, cx))
}
pub(crate) fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
@@ -484,7 +510,8 @@ impl DisplayMap {
&mut self,
to_remove: Vec<InlayId>,
to_insert: Vec<Inlay>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) {
if to_remove.is_empty() && to_insert.is_empty() {
return;
@@ -493,11 +520,11 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let tab_size = Self::tab_size(&self.buffer, model, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
@@ -505,11 +532,15 @@ impl DisplayMap {
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
.update(cx, |map, model, cx| map.sync(snapshot, edits, model, cx));
self.block_map.read(snapshot, edits);
}
fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
fn tab_size(
buffer: &Model<MultiBuffer>,
model: &Model<Self>,
cx: &mut AppContext,
) -> NonZeroU32 {
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
let language = buffer
.and_then(|buffer| buffer.language())
@@ -1458,7 +1489,7 @@ pub mod tests {
}
});
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -1469,6 +1500,7 @@ pub mod tests {
excerpt_header_height,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
@@ -1476,7 +1508,7 @@ pub mod tests {
let mut fold_count = 0;
let mut blocks = Vec::new();
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
@@ -1493,7 +1525,9 @@ pub mod tests {
Some(px(rng.gen_range(0.0..=max_wrap_width)))
};
log::info!("setting wrap width to {:?}", wrap_width);
map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
map.update(cx, |map, model, cx| {
map.set_wrap_width(wrap_width, model, cx)
});
}
20..=29 => {
let mut tab_sizes = vec![1, 2, 3, 4];
@@ -1509,9 +1543,9 @@ pub mod tests {
});
}
30..=44 => {
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
if rng.gen() || blocks.is_empty() {
let buffer = map.snapshot(cx).buffer_snapshot;
let buffer = map.snapshot(model, cx).buffer_snapshot;
let block_properties = (0..rng.gen_range(1..=1))
.map(|_| {
let position =
@@ -1541,7 +1575,7 @@ pub mod tests {
}
})
.collect::<Vec<_>>();
blocks.extend(map.insert_blocks(block_properties, cx));
blocks.extend(map.insert_blocks(block_properties, model, cx));
} else {
blocks.shuffle(&mut rng);
let remove_count = rng.gen_range(1..=4.min(blocks.len()));
@@ -1549,7 +1583,7 @@ pub mod tests {
.map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
.collect();
log::info!("removing block ids {:?}", block_ids_to_remove);
map.remove_blocks(block_ids_to_remove, cx);
map.remove_blocks(block_ids_to_remove, model, cx);
}
});
}
@@ -1566,24 +1600,27 @@ pub mod tests {
if rng.gen() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.unfold_intersecting(ranges, true, cx);
map.update(cx, |map, model, cx| {
map.unfold_intersecting(ranges, true, model, cx);
});
} else {
log::info!("folding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
map.fold(
ranges
.into_iter()
.map(|range| Crease::simple(range, FoldPlaceholder::test()))
.collect(),
model,
cx,
);
});
}
}
_ => {
buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.randomly_mutate(&mut rng, 5, model, cx)
});
}
}
@@ -1591,7 +1628,7 @@ pub mod tests {
notifications.next().await.unwrap();
}
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
@@ -1701,14 +1738,14 @@ pub mod tests {
_ = cx.update_window(window, |_, cx| {
let text_layout_details =
editor.update(cx, |editor, cx| editor.text_layout_details(cx));
editor.update(cx, |editor, model, cx| editor.text_layout_details(cx));
let font_size = px(12.0);
let wrap_width = Some(px(64.));
let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -1719,11 +1756,12 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
assert_eq!(
snapshot.text_chunks(DisplayRow(0)).collect::<String>(),
"one two \nthree four \nfive\nsix seven \neight"
@@ -1788,22 +1826,22 @@ pub mod tests {
);
let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit([(ix..ix, "and ")], None, cx);
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(ix..ix, "and ")], None, model, cx);
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
assert_eq!(
snapshot.text_chunks(DisplayRow(1)).collect::<String>(),
"three four \nfive\nsix and \nseven eight"
);
// Re-wrap on font size changes
map.update(cx, |map, cx| {
map.set_font(font("Helvetica"), px(font_size.0 + 3.), cx)
map.update(cx, |map, model, cx| {
map.set_font(font("Helvetica"), px(font_size.0 + 3.), model, cx)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
assert_eq!(
snapshot.text_chunks(DisplayRow(1)).collect::<String>(),
"three \nfour five\nsix and \nseven \neight"
@@ -1819,7 +1857,7 @@ pub mod tests {
let buffer = MultiBuffer::build_simple(&text, cx);
let font_size = px(14.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -1830,11 +1868,12 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit(
vec![
(
@@ -1851,12 +1890,13 @@ pub mod tests {
),
],
None,
model,
cx,
)
});
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx))
map.update(cx, |map, model, cx| map.snapshot(model, cx))
.text_chunks(DisplayRow(1))
.collect::<String>()
.lines()
@@ -1864,7 +1904,7 @@ pub mod tests {
Some(" b bbbbb")
);
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx))
map.update(cx, |map, model, cx| map.snapshot(model, cx))
.text_chunks(DisplayRow(2))
.collect::<String>()
.lines()
@@ -1909,13 +1949,15 @@ pub mod tests {
cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(text, model, cx).with_language(language, model, cx)
});
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let font_size = px(14.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer,
font("Helvetica"),
@@ -1926,6 +1968,7 @@ pub mod tests {
1,
1,
FoldPlaceholder::test(),
model,
cx,
)
});
@@ -1949,12 +1992,13 @@ pub mod tests {
]
);
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
map.fold(
vec![Crease::simple(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
FoldPlaceholder::test(),
)],
model,
cx,
)
});
@@ -2012,12 +2056,14 @@ pub mod tests {
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(text, model, cx).with_language(language, model, cx)
});
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer,
font("Courier"),
@@ -2028,12 +2074,13 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
// Insert a block in the middle of a multi-line string literal
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Below(
@@ -2044,6 +2091,7 @@ pub mod tests {
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
model,
cx,
)
});
@@ -2086,9 +2134,9 @@ pub mod tests {
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
let buffer = cx.new_model(|model, cx| Buffer::local(text, model, cx));
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.update_diagnostics(
LanguageServerId(0),
DiagnosticSet::new(
@@ -2103,14 +2151,15 @@ pub mod tests {
}],
buffer,
),
model,
cx,
)
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer,
font("Courier"),
@@ -2121,6 +2170,7 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
@@ -2129,7 +2179,7 @@ pub mod tests {
let red = gpui::red().to_rgb();
// Insert a block in the middle of a multi-line diagnostic.
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
map.highlight_text(
TypeId::of::<usize>(),
vec![
@@ -2150,11 +2200,12 @@ pub mod tests {
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
model,
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
let mut chunks = Vec::<(String, Option<DiagnosticSeverity>, Rgba)>::new();
for chunk in snapshot.chunks(DisplayRow(0)..DisplayRow(5), true, Default::default()) {
let color = chunk
@@ -2199,7 +2250,7 @@ pub mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Courier"),
@@ -2210,11 +2261,12 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
let snapshot = map.update(cx, |map, cx| {
let snapshot = map.update(cx, |map, model, cx| {
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Replace(
@@ -2226,9 +2278,10 @@ pub mod tests {
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
model,
cx,
);
map.snapshot(cx)
map.snapshot(model, cx)
});
assert_eq!(snapshot.text(), "abcde\n\n\n\n\npqrst");
@@ -2337,13 +2390,15 @@ pub mod tests {
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(text, model, cx).with_language(language, model, cx)
});
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let font_size = px(16.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer,
font("Courier"),
@@ -2354,6 +2409,7 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
@@ -2370,12 +2426,13 @@ pub mod tests {
[("{}\n\n".to_string(), None)]
);
map.update(cx, |map, cx| {
map.update(cx, |map, model, cx| {
map.fold(
vec![Crease::simple(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
FoldPlaceholder::test(),
)],
model,
cx,
)
});
@@ -2420,14 +2477,16 @@ pub mod tests {
let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new_model(|model, cx| {
Buffer::local(text, model, cx).with_language(language, model, cx)
});
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer = cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_size = px(16.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer,
font("Courier"),
@@ -2438,6 +2497,7 @@ pub mod tests {
1,
1,
FoldPlaceholder::test(),
model,
cx,
)
});
@@ -2449,7 +2509,7 @@ pub mod tests {
..Default::default()
};
map.update(cx, |map, _cx| {
map.update(cx, |map, model, _cx| {
map.highlight_text(
TypeId::of::<MyType>(),
highlighted_ranges
@@ -2553,7 +2613,7 @@ pub mod tests {
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
let buffer = MultiBuffer::build_simple(text, cx);
let font_size = px(14.0);
cx.new_model(|cx| {
cx.new_model(|model, cx| {
let mut map = DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -2564,6 +2624,7 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
);
let snapshot = map.buffer.read(cx).snapshot(cx);
@@ -2592,7 +2653,7 @@ pub mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let font_size = px(14.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -2603,10 +2664,11 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
let map = map.update(cx, |map, cx| map.snapshot(cx));
let map = map.update(cx, |map, model, cx| map.snapshot(model, cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ");
assert_eq!(
map.text_chunks(DisplayRow(0)).collect::<String>(),
@@ -2669,7 +2731,7 @@ pub mod tests {
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_size = px(14.0);
let map = cx.new_model(|cx| {
let map = cx.new_model(|model, cx| {
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
@@ -2680,11 +2742,13 @@ pub mod tests {
1,
0,
FoldPlaceholder::test(),
model,
cx,
)
});
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
map.update(cx, |map, model, cx| map.snapshot(model, cx))
.max_point(),
DisplayPoint::new(DisplayRow(1), 11)
)
}
@@ -2707,7 +2771,7 @@ pub mod tests {
theme: &SyntaxTheme,
cx: &mut AppContext,
) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = map.update(cx, |map, model, cx| map.snapshot(model, cx));
let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
for chunk in snapshot.chunks(rows, true, HighlightStyles::default()) {
let syntax_color = chunk

View File

@@ -4,7 +4,7 @@ use super::{
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
use gpui::{AnyElement, EntityId, Pixels};
use language::{Chunk, Patch, Point};
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToOffset, ToPoint as _,
@@ -22,7 +22,7 @@ use std::{
};
use sum_tree::{Bias, SumTree, Summary, TreeMap};
use text::Edit;
use ui::ElementId;
use ui::{AppContext, ElementId};
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
const BULLETS: &str = "********************************************************************************************************************************";
@@ -258,7 +258,8 @@ pub enum BlockStyle {
}
pub struct BlockContext<'a, 'b> {
pub context: &'b mut WindowContext<'a>,
pub window: &'a mut Window,
pub context: &'b mut AppContext,
pub anchor_x: Pixels,
pub max_width: Pixels,
pub gutter_dimensions: &'b GutterDimensions,
@@ -1686,8 +1687,8 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow {
}
}
impl<'a> Deref for BlockContext<'a, '_> {
type Target = WindowContext<'a>;
impl Deref for BlockContext<'_, '_> {
type Target = AppContext;
fn deref(&self) -> &Self::Target {
self.context
@@ -1784,7 +1785,7 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
@@ -1926,7 +1927,7 @@ mod tests {
);
// Insert a line break, separating two block decorations into separate lines.
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
buffer.snapshot(cx)
});
@@ -1936,7 +1937,7 @@ mod tests {
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
@@ -1947,12 +1948,12 @@ mod tests {
fn test_multibuffer_headers_and_footers(cx: &mut AppContext) {
init_test(cx);
let buffer1 = cx.new_model(|cx| Buffer::local("Buffer 1", cx));
let buffer2 = cx.new_model(|cx| Buffer::local("Buffer 2", cx));
let buffer3 = cx.new_model(|cx| Buffer::local("Buffer 3", cx));
let buffer1 = cx.new_model(|model, cx| Buffer::local("Buffer 1", cx));
let buffer2 = cx.new_model(|model, cx| Buffer::local("Buffer 2", cx));
let buffer3 = cx.new_model(|model, cx| Buffer::local("Buffer 3", cx));
let mut excerpt_ids = Vec::new();
let multi_buffer = cx.new_model(|cx| {
let multi_buffer = cx.new_model(|model, cx| {
let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
excerpt_ids.extend(multi_buffer.push_excerpts(
buffer1.clone(),
@@ -2032,7 +2033,7 @@ mod tests {
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let _subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
@@ -2177,7 +2178,7 @@ mod tests {
let text = "line1\nline2\nline3\nline4\nline5";
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_subscription = buffer.update(cx, |buffer, _cx| buffer.subscribe());
let buffer_subscription = buffer.update(cx, |buffer, model, _cx| buffer.subscribe());
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
@@ -2202,7 +2203,7 @@ mod tests {
let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
buffer.snapshot(cx)
});
@@ -2212,13 +2213,13 @@ mod tests {
);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit(
[(
Point::new(1, 5)..Point::new(1, 5),
@@ -2235,7 +2236,7 @@ mod tests {
);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
@@ -2354,7 +2355,7 @@ mod tests {
Some(px(rng.gen_range(0.0..=100.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
wrap_map.update(cx, |map, model, cx| map.set_wrap_width(wrap_width, cx));
}
20..=39 => {
let block_count = rng.gen_range(1..=5);
@@ -2399,9 +2400,10 @@ mod tests {
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let (wraps_snapshot, wrap_edits) = wrap_map
.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.insert(block_properties.iter().map(|props| BlockProperties {
placement: props.placement.clone(),
@@ -2424,14 +2426,15 @@ mod tests {
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let (wraps_snapshot, wrap_edits) = wrap_map
.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
}
_ => {
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let mutation_count = rng.gen_range(1..=5);
let subscription = buffer.subscribe();
buffer.randomly_mutate(&mut rng, mutation_count, cx);
@@ -2446,7 +2449,7 @@ mod tests {
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, model, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point;
use ui::{IconName, SharedString, WindowContext};
use ui::{IconName, SharedString};
use crate::{BlockStyle, FoldPlaceholder, RenderBlock};
@@ -117,12 +117,16 @@ type RenderToggleFn = Arc<
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
Arc<dyn Send + Sync + Fn(bool, &mut gpui::Window, &mut gpui::AppContext)>,
&mut gpui::Window,
&mut gpui::AppContext,
) -> AnyElement,
>;
type RenderTrailerFn =
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
type RenderTrailerFn = Arc<
dyn Send
+ Sync
+ Fn(MultiBufferRow, bool, &mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
>;
#[derive(Clone)]
pub enum Crease<T> {
@@ -185,15 +189,16 @@ impl<T> Crease<T> {
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
Arc<dyn Send + Sync + Fn(bool, &mut gpui::Window, &mut gpui::AppContext)>,
&mut gpui::Window,
&mut gpui::AppContext,
) -> ToggleElement
+ 'static,
ToggleElement: IntoElement,
RenderTrailer: 'static
+ Send
+ Sync
+ Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement
+ Fn(MultiBufferRow, bool, &mut gpui::Window, &mut gpui::AppContext) -> TrailerElement
+ 'static,
TrailerElement: IntoElement,
{

View File

@@ -2,7 +2,7 @@ use super::{
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
Highlights,
};
use gpui::{AnyElement, ElementId, WindowContext};
use gpui::{AnyElement, ElementId};
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
use std::{
@@ -19,7 +19,11 @@ use util::post_inc;
#[derive(Clone)]
pub struct FoldPlaceholder {
/// Creates an element to represent this fold's placeholder.
pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
pub render: Arc<
dyn Send
+ Sync
+ Fn(FoldId, Range<Anchor>, &mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
>,
/// If true, the element is constrained to the shaped width of an ellipsis.
pub constrain_width: bool,
/// If true, merges the fold with an adjacent one.
@@ -1394,7 +1398,7 @@ mod tests {
fn test_basic_folds(cx: &mut gpui::AppContext) {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1419,13 +1423,14 @@ mod tests {
]
);
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit(
vec![
(Point::new(0, 0)..Point::new(0, 1), "123"),
(Point::new(2, 3)..Point::new(2, 3), "123"),
],
None,
model,
cx,
);
buffer.snapshot(cx)
@@ -1449,8 +1454,13 @@ mod tests {
]
);
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx);
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit(
[(Point::new(2, 6)..Point::new(4, 3), "456")],
None,
model,
cx,
);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
@@ -1473,7 +1483,7 @@ mod tests {
fn test_adjacent_folds(cx: &mut gpui::AppContext) {
init_test(cx);
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
@@ -1517,8 +1527,8 @@ mod tests {
assert_eq!(snapshot.text(), "⋯fghijkl");
// Edit within one of the folds.
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([(0..1, "12345")], None, cx);
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit([(0..1, "12345")], None, model, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
@@ -1549,7 +1559,7 @@ mod tests {
fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let subscription = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1562,8 +1572,8 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
let buffer_snapshot = buffer.update(cx, |buffer, model, cx| {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, model, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
@@ -1637,10 +1647,10 @@ mod tests {
let (_, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
inlay_edits = edits;
}
_ => buffer.update(cx, |buffer, cx| {
_ => buffer.update(cx, |buffer, model, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer.randomly_mutate(&mut rng, edit_count, model, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
log::info!("editing {:?}", edits);

View File

@@ -1299,7 +1299,7 @@ mod tests {
#[gpui::test]
fn test_basic_inlays(cx: &mut AppContext) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_edits = buffer.update(cx, |buffer, model, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
assert_eq!(inlay_snapshot.text(), "abcdefghi");
let mut next_inlay_id = 0;
@@ -1363,7 +1363,7 @@ mod tests {
);
// Edits before or after the inlay should not affect it.
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
});
let (inlay_snapshot, _) = inlay_map.sync(
@@ -1373,7 +1373,7 @@ mod tests {
assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
// An edit surrounding the inlay should invalidate it.
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
buffer.update(cx, |buffer, model, cx| buffer.edit([(4..5, "D")], None, cx));
let (inlay_snapshot, _) = inlay_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
@@ -1398,7 +1398,9 @@ mod tests {
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
// Edits ending where the inlay starts should not move it if it has a left bias.
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
buffer.update(cx, |buffer, model, cx| {
buffer.edit([(3..3, "JKL")], None, cx)
});
let (inlay_snapshot, _) = inlay_map.sync(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
@@ -1650,7 +1652,7 @@ mod tests {
log::info!("mutated text: {:?}", snapshot.text());
inlay_edits = Patch::new(edits);
}
_ => buffer.update(cx, |buffer, cx| {
_ => buffer.update(cx, |buffer, model, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);

View File

@@ -3,7 +3,7 @@ use super::{
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
use gpui::{AppContext, Context, Font, LineWrapper, Model, Pixels, Task};
use language::{Chunk, Point};
use multi_buffer::MultiBufferSnapshot;
use smol::future::yield_now;
@@ -92,7 +92,7 @@ impl WrapMap {
wrap_width: Option<Pixels>,
cx: &mut AppContext,
) -> (Model<Self>, WrapSnapshot) {
let handle = cx.new_model(|cx| {
let handle = cx.new_model(|model, cx| {
let mut this = Self {
font_with_size: (font, font_size),
wrap_width: None,
@@ -102,7 +102,7 @@ impl WrapMap {
snapshot: WrapSnapshot::new(tab_snapshot),
background_task: None,
};
this.set_wrap_width(wrap_width, cx);
this.set_wrap_width(wrap_width, model, cx);
mem::take(&mut this.edits_since_sync);
this
});
@@ -119,7 +119,8 @@ impl WrapMap {
&mut self,
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((tab_snapshot, edits));
@@ -138,7 +139,8 @@ impl WrapMap {
&mut self,
font: Font,
font_size: Pixels,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> bool {
let font_with_size = (font, font_size);
@@ -154,7 +156,8 @@ impl WrapMap {
pub fn set_wrap_width(
&mut self,
wrap_width: Option<Pixels>,
cx: &mut ModelContext<Self>,
model: &Model<Self>,
cx: &mut AppContext,
) -> bool {
if wrap_width == self.wrap_width {
return false;
@@ -165,7 +168,7 @@ impl WrapMap {
true
}
fn rewrap(&mut self, cx: &mut ModelContext<Self>) {
fn rewrap(&mut self, model: &Model<Self>, cx: &mut AppContext) {
self.background_task.take();
self.interpolated_edits.clear();
self.pending_edits.clear();
@@ -202,9 +205,9 @@ impl WrapMap {
self.edits_since_sync = self.edits_since_sync.compose(&edits);
}
Err(wrap_task) => {
self.background_task = Some(cx.spawn(|this, mut cx| async move {
self.background_task = Some(model.spawn(cx, |this, mut cx| async move {
let (snapshot, edits) = wrap_task.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.snapshot = snapshot;
this.edits_since_sync = this
.edits_since_sync
@@ -212,7 +215,7 @@ impl WrapMap {
.compose(&edits);
this.background_task = None;
this.flush_edits(cx);
cx.notify();
model.notify(cx);
})
.ok();
}));
@@ -236,7 +239,7 @@ impl WrapMap {
}
}
fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
fn flush_edits(&mut self, model: &Model<Self>, cx: &mut AppContext) {
if !self.snapshot.interpolated {
let mut to_remove_len = 0;
for (tab_snapshot, _) in &self.pending_edits {
@@ -280,9 +283,9 @@ impl WrapMap {
self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
}
Err(update_task) => {
self.background_task = Some(cx.spawn(|this, mut cx| async move {
self.background_task = Some(model.spawn(cx, |this, mut cx| async move {
let (snapshot, edits) = update_task.await;
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |this, model, cx| {
this.snapshot = snapshot;
this.edits_since_sync = this
.edits_since_sync
@@ -290,7 +293,7 @@ impl WrapMap {
.compose(&edits);
this.background_task = None;
this.flush_edits(cx);
cx.notify();
model.notify(cx);
})
.ok();
}));
@@ -1229,9 +1232,9 @@ mod tests {
notifications.next().await.unwrap();
}
let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
let (initial_snapshot, _) = wrap_map.update(cx, |map, model, cx| {
assert!(!map.is_rewrapping());
map.sync(tabs_snapshot.clone(), Vec::new(), cx)
map.sync(tabs_snapshot.clone(), Vec::new(), model, cx)
});
let actual_text = initial_snapshot.text();
@@ -1256,14 +1259,17 @@ mod tests {
Some(px(rng.gen_range(0.0..=1000.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
wrap_map.update(cx, |map, model, cx| {
map.set_wrap_width(wrap_width, model, cx)
});
}
20..=39 => {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
let (tabs_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
let (mut snapshot, wrap_edits) = wrap_map.update(cx, |map, model, cx| {
map.sync(tabs_snapshot, tab_edits, model, cx)
});
snapshot.check_invariants();
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
@@ -1275,17 +1281,18 @@ mod tests {
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tabs_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
let (mut snapshot, wrap_edits) = wrap_map.update(cx, |map, model, cx| {
map.sync(tabs_snapshot, tab_edits, model, cx)
});
snapshot.check_invariants();
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
}
_ => {
buffer.update(cx, |buffer, cx| {
buffer.update(cx, |buffer, model, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer.randomly_mutate(&mut rng, edit_count, model, cx);
buffer_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume());
});
@@ -1303,8 +1310,9 @@ mod tests {
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
let (mut snapshot, wrap_edits) = wrap_map.update(cx, |map, model, cx| {
map.sync(tabs_snapshot.clone(), tab_edits, model, cx)
});
snapshot.check_invariants();
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
@@ -1318,8 +1326,9 @@ mod tests {
}
if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
let (mut wrapped_snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
let (mut wrapped_snapshot, wrap_edits) = wrap_map.update(cx, |map, model, cx| {
map.sync(tabs_snapshot, Vec::new(), model, cx)
});
let actual_text = wrapped_snapshot.text();
let actual_longest_row = wrapped_snapshot.longest_row();
log::info!("Wrapping finished: {:?}", actual_text);

Some files were not shown because too many files have changed in this diff Show More