Merge main

This commit is contained in:
Remco Smits
2024-10-07 20:11:42 +02:00
151 changed files with 2243 additions and 1931 deletions

View File

@@ -7,9 +7,13 @@ on:
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
paths-ignore:
- "docs/**"
pull_request:
branches:
- "**"
paths-ignore:
- "docs/**"
concurrency:
# Allow only one workflow per any non-`main` branch.

View File

@@ -20,11 +20,14 @@ jobs:
with:
version: 9
- run: |
- name: Prettier Check on /docs
working-directory: ./docs
run: |
pnpm dlx prettier . --check || {
echo "To fix, run from the root of the zed repo:"
echo " cd docs && pnpm dlx prettier . --write && cd .."
false
}
working-directory: ./docs
- name: Check spelling
run: script/check-spelling docs/

15
Cargo.lock generated
View File

@@ -9055,7 +9055,6 @@ dependencies = [
"gpui",
"language",
"log",
"markdown",
"menu",
"ordered-float 2.10.1",
"picker",
@@ -9218,6 +9217,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"cargo_toml",
"clap",
"client",
"clock",
"env_logger",
@@ -10764,6 +10764,7 @@ dependencies = [
"parking_lot",
"project",
"smol",
"sqlformat",
"thread_local",
"util",
"uuid",
@@ -11051,6 +11052,7 @@ dependencies = [
"theme",
"title_bar",
"ui",
"ureq_client",
]
[[package]]
@@ -11552,6 +11554,7 @@ dependencies = [
name = "telemetry_events"
version = "0.1.0"
dependencies = [
"language",
"semantic_version",
"serde",
]
@@ -14377,6 +14380,7 @@ dependencies = [
"parking_lot",
"postage",
"project",
"release_channel",
"remote",
"schemars",
"serde",
@@ -14905,7 +14909,7 @@ dependencies = [
[[package]]
name = "zed_php"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -14917,6 +14921,13 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_proto"
version = "0.2.0"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_purescript"
version = "0.0.1"

View File

@@ -156,6 +156,7 @@ members = [
"extensions/php",
"extensions/perplexity",
"extensions/prisma",
"extensions/proto",
"extensions/purescript",
"extensions/ruff",
"extensions/ruby",
@@ -423,6 +424,7 @@ similar = "1.3"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
sqlformat = "0.2"
strsim = "0.11"
strum = { version = "0.25.0", features = ["derive"] }
subtle = "2.5.0"

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -440,7 +440,12 @@
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"cmd-shift-x": "zed::Extensions",
"cmd-shift-x": "zed::Extensions"
}
},
{
"context": "Workspace && !Terminal",
"bindings": {
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}

View File

@@ -1 +1,2 @@
allow-private-module-inception = true
avoid-breaking-exported-api = false

View File

@@ -77,7 +77,7 @@ use ui::TintColor;
use ui::{
prelude::*,
utils::{format_distance_from_now, DateTimeType},
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use util::{maybe, ResultExt};
@@ -262,9 +262,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
.gap_2()
.children(if let Some(host_user) = host_user {
vec![
Avatar::new(host_user.avatar_uri.clone())
.shape(AvatarShape::Circle)
.into_any_element(),
Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
Label::new(format!("Shared by @{}", host_user.github_login))
.color(Color::Muted)
.size(LabelSize::Small)

View File

@@ -46,7 +46,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::{AssistantKind, AssistantPhase};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use text::BufferSnapshot;
use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -549,7 +549,7 @@ impl Context {
cx: &mut ModelContext<Self>,
) -> Self {
let buffer = cx.new_model(|_cx| {
let mut buffer = Buffer::remote(
let buffer = Buffer::remote(
language::BufferId::new(1).unwrap(),
replica_id,
capability,
@@ -2133,14 +2133,21 @@ impl Context {
});
if let Some(telemetry) = this.telemetry.as_ref() {
telemetry.report_assistant_event(
Some(this.id.0.clone()),
AssistantKind::Panel,
AssistantPhase::Response,
model.telemetry_id(),
let language_name = this
.buffer
.read(cx)
.language()
.map(|language| language.name());
telemetry.report_assistant_event(AssistantEvent {
conversation_id: Some(this.id.0.clone()),
kind: AssistantKind::Panel,
phase: AssistantPhase::Response,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency,
error_message,
);
language_name,
});
}
if let Ok(stop_reason) = result {

View File

@@ -50,6 +50,7 @@ use std::{
task::{self, Poll},
time::{Duration, Instant},
};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
@@ -209,18 +210,6 @@ impl InlineAssistant {
initial_prompt: Option<String>,
cx: &mut WindowContext,
) {
if let Some(telemetry) = self.telemetry.as_ref() {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
telemetry_events::AssistantPhase::Invoked,
model.telemetry_id(),
None,
None,
);
}
}
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let mut selections = Vec::<Selection<Point>>::new();
@@ -267,6 +256,21 @@ impl InlineAssistant {
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
if let Some(telemetry) = self.telemetry.as_ref() {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name()),
});
}
}
}
let assist_group_id = self.next_assist_group_id.post_inc();
@@ -761,23 +765,34 @@ impl InlineAssistant {
}
pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
if let Some(telemetry) = self.telemetry.as_ref() {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
if undo {
telemetry_events::AssistantPhase::Rejected
} else {
telemetry_events::AssistantPhase::Accepted
},
model.telemetry_id(),
None,
None,
);
}
}
if let Some(assist) = self.assists.get(&assist_id) {
if let Some(telemetry) = self.telemetry.as_ref() {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.map(|language| language.name())
});
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: if undo {
AssistantPhase::Rejected
} else {
AssistantPhase::Accepted
},
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name,
});
}
}
let assist_group_id = assist.group_id;
if self.assist_groups[&assist_group_id].linked {
for assist_id in self.unlink_assist_group(assist_group_id, cx) {
@@ -2706,6 +2721,7 @@ impl CodegenAlternative {
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
let telemetry_id = model.telemetry_id();
let provider_id = model.provider_id();
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(stream::empty().boxed()) }.boxed_local()
@@ -2716,7 +2732,7 @@ impl CodegenAlternative {
.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await });
async move { Ok(chunks.await?.boxed()) }.boxed_local()
};
self.handle_stream(telemetry_id, chunks, cx);
self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx);
Ok(())
}
@@ -2780,6 +2796,7 @@ impl CodegenAlternative {
pub fn handle_stream(
&mut self,
model_telemetry_id: String,
model_provider_id: String,
stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
cx: &mut ModelContext<Self>,
) {
@@ -2810,6 +2827,15 @@ impl CodegenAlternative {
}
let telemetry = self.telemetry.clone();
let language_name = {
let multibuffer = self.buffer.read(cx);
let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.map(|language| language.name())
};
self.diff = Diff::default();
self.status = CodegenStatus::Pending;
let mut edit_start = self.range.start.to_offset(&snapshot);
@@ -2920,14 +2946,16 @@ impl CodegenAlternative {
let error_message =
result.as_ref().err().map(|error| error.to_string());
if let Some(telemetry) = telemetry {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
telemetry_events::AssistantPhase::Response,
model_telemetry_id,
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
);
language_name,
});
}
result?;
@@ -3539,6 +3567,7 @@ mod tests {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
String::new(),
future::ready(Ok(chunks_rx.map(Ok).boxed())),
cx,
@@ -3610,6 +3639,7 @@ mod tests {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
String::new(),
future::ready(Ok(chunks_rx.map(Ok).boxed())),
cx,
@@ -3684,6 +3714,7 @@ mod tests {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
String::new(),
future::ready(Ok(chunks_rx.map(Ok).boxed())),
cx,
@@ -3757,6 +3788,7 @@ mod tests {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
String::new(),
future::ready(Ok(chunks_rx.map(Ok).boxed())),
cx,
@@ -3820,6 +3852,7 @@ mod tests {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
String::new(),
future::ready(Ok(chunks_rx.map(Ok).boxed())),
cx,

View File

@@ -910,7 +910,7 @@ impl PromptLibrary {
.features
.clone(),
font_size: HeadlineSize::Large
.size()
.rems()
.into(),
font_weight: settings.ui_font.weight,
line_height: relative(

View File

@@ -25,6 +25,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
@@ -1039,6 +1040,7 @@ impl Codegen {
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
self.generation = cx.spawn(|this, mut cx| async move {
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
let response = model.stream_completion_text(prompt, &cx).await;
let generate = async {
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
@@ -1063,14 +1065,16 @@ impl Codegen {
let error_message = result.as_ref().err().map(|error| error.to_string());
if let Some(telemetry) = telemetry {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
telemetry_events::AssistantPhase::Response,
model_telemetry_id,
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Response,
model: model_telemetry_id,
model_provider: model_provider_id.to_string(),
response_latency,
error_message,
);
language_name: None,
});
}
result?;

View File

@@ -394,7 +394,7 @@ pub struct PendingEntitySubscription<T: 'static> {
}
impl<T: 'static> PendingEntitySubscription<T> {
pub fn set_model(mut self, model: &Model<T>, cx: &mut AsyncAppContext) -> Subscription {
pub fn set_model(mut self, model: &Model<T>, cx: &AsyncAppContext) -> Subscription {
self.consumed = true;
let mut handlers = self.client.handler_set.lock();
let id = (TypeId::of::<T>(), self.remote_id);

View File

@@ -16,9 +16,9 @@ use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, AssistantPhase, CallEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent,
InlineCompletionEvent, MemoryEvent, ReplEvent, SettingEvent,
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
SettingEvent,
};
use tempfile::NamedTempFile;
#[cfg(not(debug_assertions))]
@@ -288,7 +288,7 @@ impl Telemetry {
system_id: Option<String>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
cx: &AppContext,
) {
let mut state = self.state.lock();
state.system_id = system_id.map(|id| id.into());
@@ -391,25 +391,8 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_assistant_event(
self: &Arc<Self>,
conversation_id: Option<String>,
kind: AssistantKind,
phase: AssistantPhase,
model: String,
response_latency: Option<Duration>,
error_message: Option<String>,
) {
let event = Event::Assistant(AssistantEvent {
conversation_id,
kind,
phase,
model: model.to_string(),
response_latency,
error_message,
});
self.report_event(event)
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}
pub fn report_call_event(

View File

@@ -138,7 +138,7 @@ enum UpdateContacts {
}
impl UserStore {
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
pub fn new(client: Arc<Client>, cx: &ModelContext<Self>) -> 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![
@@ -310,7 +310,7 @@ impl UserStore {
fn update_contacts(
&mut self,
message: UpdateContacts,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
@@ -525,9 +525,9 @@ impl UserStore {
}
pub fn dismiss_contact_request(
&mut self,
&self,
requester_id: u64,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn(move |_, _| async move {
@@ -573,7 +573,7 @@ impl UserStore {
})
}
pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
pub fn clear_contacts(&self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Clear(tx))
@@ -583,7 +583,7 @@ impl UserStore {
}
}
pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
pub fn contact_updates_done(&self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Wait(tx))
@@ -594,9 +594,9 @@ impl UserStore {
}
pub fn get_users(
&mut self,
&self,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> 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));
@@ -629,9 +629,9 @@ impl UserStore {
}
pub fn fuzzy_search_users(
&mut self,
&self,
query: String,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
@@ -640,11 +640,7 @@ impl UserStore {
self.users.get(&user_id).cloned()
}
pub fn get_user_optimistic(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Option<Arc<User>> {
pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext<Self>) -> Option<Arc<User>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Some(user);
}
@@ -653,11 +649,7 @@ impl UserStore {
None
}
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<Arc<User>>> {
pub fn get_user(&self, user_id: u64, cx: &ModelContext<Self>) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Task::ready(Ok(user));
}
@@ -697,7 +689,7 @@ impl UserStore {
.map(|accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
pub fn accept_terms_of_service(&self, cx: &ModelContext<Self>) -> Task<Result<()>> {
if self.current_user().is_none() {
return Task::ready(Err(anyhow!("no current user")));
};
@@ -726,9 +718,9 @@ impl UserStore {
}
fn load_users(
&mut self,
&self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
cx.spawn(|this, mut cx| async move {

View File

@@ -835,7 +835,7 @@ impl TestClient {
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
ssh: Arc<SshRemoteClient>,
ssh: Model<SshRemoteClient>,
cx: &mut TestAppContext,
) -> (Model<Project>, WorktreeId) {
let project = cx.update(|cx| {

View File

@@ -188,7 +188,7 @@ macro_rules! define_connection {
};
}
pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
pub fn write_and_log<F>(cx: &AppContext, db_write: impl FnOnce() -> F + Send + 'static)
where
F: Future<Output = anyhow::Result<()>> + Send,
{

View File

@@ -11167,7 +11167,7 @@ impl Editor {
.selections
.all::<Point>(cx)
.iter()
.any(|selection| selection.range().overlaps(&intersection_range));
.any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range));
self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
}

View File

@@ -8,7 +8,7 @@ use gpui::AppContext;
pub use crate::providers::*;
/// Initializes the Git hosting providers.
pub fn init(cx: &mut AppContext) {
pub fn init(cx: &AppContext) {
let provider_registry = GitHostingProviderRegistry::global(cx);
// The providers are stored in a `BTreeMap`, so insertion order matters.

View File

@@ -348,7 +348,7 @@ impl AppContext {
}
/// Gracefully quit the application via the platform's standard routine.
pub fn quit(&mut self) {
pub fn quit(&self) {
self.platform.quit();
}
@@ -1004,11 +1004,7 @@ impl AppContext {
self.globals_by_type.insert(global_type, lease.global);
}
pub(crate) fn new_view_observer(
&mut self,
key: TypeId,
value: NewViewListener,
) -> Subscription {
pub(crate) fn new_view_observer(&self, key: TypeId, value: NewViewListener) -> Subscription {
let (subscription, activate) = self.new_view_observers.insert(key, value);
activate();
subscription
@@ -1016,7 +1012,7 @@ impl AppContext {
/// Arrange for the given function to be invoked whenever a view of the specified type is created.
/// The function will be passed a mutable reference to the view along with an appropriate context.
pub fn observe_new_views<V: 'static>(
&mut self,
&self,
on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
) -> Subscription {
self.new_view_observer(
@@ -1035,7 +1031,7 @@ impl AppContext {
/// Observe the release of a model or view. The callback is invoked after the model or view
/// has no more strong references but before it has been dropped.
pub fn observe_release<E, T>(
&mut self,
&self,
handle: &E,
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
) -> Subscription
@@ -1062,7 +1058,7 @@ impl AppContext {
mut f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static,
) -> Subscription {
fn inner(
keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>,
keystroke_observers: &SubscriberSet<(), KeystrokeObserver>,
handler: KeystrokeObserver,
) -> Subscription {
let (subscription, activate) = keystroke_observers.insert((), handler);
@@ -1140,7 +1136,7 @@ impl AppContext {
/// Register a callback to be invoked when the application is about to quit.
/// It is not possible to cancel the quit event at this point.
pub fn on_app_quit<Fut>(
&mut self,
&self,
mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
) -> Subscription
where
@@ -1186,7 +1182,7 @@ impl AppContext {
}
/// Sets the menu bar for this application. This will replace any existing menu bar.
pub fn set_menus(&mut self, menus: Vec<Menu>) {
pub fn set_menus(&self, menus: Vec<Menu>) {
self.platform.set_menus(menus, &self.keymap.borrow());
}
@@ -1196,7 +1192,7 @@ impl AppContext {
}
/// Sets the right click menu for the app icon in the dock
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
pub fn set_dock_menu(&self, menus: Vec<MenuItem>) {
self.platform.set_dock_menu(menus, &self.keymap.borrow());
}
@@ -1204,7 +1200,7 @@ impl AppContext {
/// The list is usually shown on the application icon's context menu in the dock,
/// and allows to open the recent files via that context menu.
/// If the path is already in the list, it will be moved to the bottom of the list.
pub fn add_recent_document(&mut self, path: &Path) {
pub fn add_recent_document(&self, path: &Path) {
self.platform.add_recent_document(path);
}

View File

@@ -107,7 +107,7 @@ impl Context for AsyncAppContext {
impl AsyncAppContext {
/// Schedules all windows in the application to be redrawn.
pub fn refresh(&mut self) -> Result<()> {
pub fn refresh(&self) -> Result<()> {
let app = self
.app
.upgrade()
@@ -205,7 +205,7 @@ impl AsyncAppContext {
/// A convenience method for [AppContext::update_global]
/// for updating the global state of the specified type.
pub fn update_global<G: Global, R>(
&mut self,
&self,
update: impl FnOnce(&mut G, &mut AppContext) -> R,
) -> Result<R> {
let app = self

View File

@@ -91,7 +91,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
/// Register a callback to be invoked when GPUI releases this model.
pub fn on_release(
&mut self,
&self,
on_release: impl FnOnce(&mut T, &mut AppContext) + 'static,
) -> Subscription
where
@@ -110,7 +110,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
/// Register a callback to be run on the release of another model or view
pub fn observe_release<T2, E>(
&mut self,
&self,
entity: &E,
on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static,
) -> Subscription
@@ -154,7 +154,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
/// Arrange for the given function to be invoked whenever the application is quit.
/// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits.
pub fn on_app_quit<Fut>(
&mut self,
&self,
mut on_quit: impl FnMut(&mut T, &mut ModelContext<T>) -> Fut + 'static,
) -> Subscription
where

View File

@@ -1418,7 +1418,7 @@ impl Interactivity {
}
fn clamp_scroll_position(
&mut self,
&self,
bounds: Bounds<Pixels>,
style: &Style,
cx: &mut WindowContext,
@@ -1547,7 +1547,7 @@ impl Interactivity {
#[cfg(debug_assertions)]
fn paint_debug_info(
&mut self,
&self,
global_id: Option<&GlobalElementId>,
hitbox: &Hitbox,
style: &Style,

View File

@@ -252,7 +252,7 @@ impl TextLayout {
}
fn layout(
&mut self,
&self,
text: SharedString,
runs: Option<Vec<TextRun>>,
cx: &mut WindowContext,
@@ -350,7 +350,7 @@ impl TextLayout {
layout_id
}
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
let mut element_state = self.lock();
let element_state = element_state
.as_mut()
@@ -359,7 +359,7 @@ impl TextLayout {
element_state.bounds = Some(bounds);
}
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
fn paint(&self, text: &str, cx: &mut WindowContext) {
let element_state = self.lock();
let element_state = element_state
.as_ref()

View File

@@ -115,7 +115,7 @@ impl UniformListScrollHandle {
}
/// Scroll the list to the given item index.
pub fn scroll_to_item(&mut self, ix: usize) {
pub fn scroll_to_item(&self, ix: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(ix);
}
@@ -297,7 +297,11 @@ impl Element for UniformList {
for (mut item, ix) in items.into_iter().zip(visible_range) {
let item_origin = padded_bounds.origin
+ point(
scroll_offset.x + padding.left,
if can_scroll_horizontally {
scroll_offset.x + padding.left
} else {
scroll_offset.x
},
item_height * ix + scroll_offset.y + padding.top,
);
let available_width = if can_scroll_horizontally {

View File

@@ -706,11 +706,7 @@ pub struct Bounds<T: Clone + Default + Debug> {
impl Bounds<Pixels> {
/// Generate a centered bounds for the given display or primary display if none is provided
pub fn centered(
display_id: Option<DisplayId>,
size: Size<Pixels>,
cx: &mut AppContext,
) -> Self {
pub fn centered(display_id: Option<DisplayId>, size: Size<Pixels>, cx: &AppContext) -> Self {
let display = display_id
.and_then(|id| cx.find_display(id))
.or_else(|| cx.primary_display());
@@ -730,7 +726,7 @@ impl Bounds<Pixels> {
}
/// Generate maximized bounds for the given display or primary display if none is provided
pub fn maximized(display_id: Option<DisplayId>, cx: &mut AppContext) -> Self {
pub fn maximized(display_id: Option<DisplayId>, cx: &AppContext) -> Self {
let display = display_id
.and_then(|id| cx.find_display(id))
.or_else(|| cx.primary_display());

View File

@@ -219,7 +219,7 @@ impl DispatchTree {
self.focusable_node_ids.insert(focus_id, node_id);
}
pub fn parent_view_id(&mut self) -> Option<EntityId> {
pub fn parent_view_id(&self) -> Option<EntityId> {
self.view_stack.last().copied()
}
@@ -484,7 +484,7 @@ impl DispatchTree {
/// Converts the longest prefix of input to a replay event and returns the rest.
fn replay_prefix(
&mut self,
&self,
mut input: SmallVec<[Keystroke; 1]>,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) {

View File

@@ -171,7 +171,7 @@ pub enum OsAction {
Redo,
}
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) {
pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &AppContext) {
platform.on_will_open_app_menu(Box::new({
let cx = cx.to_async();
move || {

View File

@@ -284,11 +284,11 @@ impl MetalRenderer {
}
}
pub fn update_transparency(&mut self, _transparent: bool) {
pub fn update_transparency(&self, _transparent: bool) {
// todo(mac)?
}
pub fn destroy(&mut self) {
pub fn destroy(&self) {
// nothing to do
}
@@ -486,7 +486,7 @@ impl MetalRenderer {
}
fn rasterize_paths(
&mut self,
&self,
paths: &[Path<ScaledPixels>],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
@@ -576,7 +576,7 @@ impl MetalRenderer {
}
fn draw_shadows(
&mut self,
&self,
shadows: &[Shadow],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
@@ -639,7 +639,7 @@ impl MetalRenderer {
}
fn draw_quads(
&mut self,
&self,
quads: &[Quad],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
@@ -698,7 +698,7 @@ impl MetalRenderer {
}
fn draw_paths(
&mut self,
&self,
paths: &[Path<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
instance_buffer: &mut InstanceBuffer,
@@ -808,7 +808,7 @@ impl MetalRenderer {
}
fn draw_underlines(
&mut self,
&self,
underlines: &[Underline],
instance_buffer: &mut InstanceBuffer,
instance_offset: &mut usize,
@@ -871,7 +871,7 @@ impl MetalRenderer {
}
fn draw_monochrome_sprites(
&mut self,
&self,
texture_id: AtlasTextureId,
sprites: &[MonochromeSprite],
instance_buffer: &mut InstanceBuffer,
@@ -945,7 +945,7 @@ impl MetalRenderer {
}
fn draw_polychrome_sprites(
&mut self,
&self,
texture_id: AtlasTextureId,
sprites: &[PolychromeSprite],
instance_buffer: &mut InstanceBuffer,

View File

@@ -1432,7 +1432,7 @@ impl UTType {
self.0
}
fn inner_mut(&mut self) -> *mut Object {
fn inner_mut(&self) -> *mut Object {
self.0 as *mut _
}
}

View File

@@ -835,10 +835,7 @@ impl Window {
prompt: None,
})
}
fn new_focus_listener(
&mut self,
value: AnyWindowFocusListener,
) -> (Subscription, impl FnOnce()) {
fn new_focus_listener(&self, value: AnyWindowFocusListener) -> (Subscription, impl FnOnce()) {
self.focus_listeners.insert((), value)
}
}
@@ -929,7 +926,7 @@ impl<'a> WindowContext<'a> {
/// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus
/// for elements rendered within this window.
pub fn focus_handle(&mut self) -> FocusHandle {
pub fn focus_handle(&self) -> FocusHandle {
FocusHandle::new(&self.window.focus_handles)
}
@@ -1127,7 +1124,7 @@ impl<'a> WindowContext<'a> {
/// Register a callback to be invoked when the given Model or View is released.
pub fn observe_release<E, T>(
&mut self,
&self,
entity: &E,
mut on_release: impl FnOnce(&mut T, &mut WindowContext) + 'static,
) -> Subscription
@@ -1155,7 +1152,7 @@ impl<'a> WindowContext<'a> {
}
/// Schedule the given closure to be run directly after the current frame is rendered.
pub fn on_next_frame(&mut self, callback: impl FnOnce(&mut WindowContext) + 'static) {
pub fn on_next_frame(&self, callback: impl FnOnce(&mut WindowContext) + 'static) {
RefCell::borrow_mut(&self.window.next_frame_callbacks).push(Box::new(callback));
}
@@ -1165,7 +1162,7 @@ impl<'a> WindowContext<'a> {
/// It will cause the window to redraw on the next frame, even if no other changes have occurred.
///
/// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window.
pub fn request_animation_frame(&mut self) {
pub fn request_animation_frame(&self) {
let parent_id = self.parent_view_id();
self.on_next_frame(move |cx| {
if let Some(parent_id) = parent_id {
@@ -1179,7 +1176,7 @@ impl<'a> WindowContext<'a> {
/// Spawn the future returned by the given closure on the application thread pool.
/// The closure is provided a handle to the current window and an `AsyncWindowContext` for
/// use within your future.
pub fn spawn<Fut, R>(&mut self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task<R>
where
R: 'static,
Fut: Future<Output = R> + 'static,
@@ -2865,7 +2862,7 @@ impl<'a> WindowContext<'a> {
}
/// Get the last view id for the current element
pub fn parent_view_id(&mut self) -> Option<EntityId> {
pub fn parent_view_id(&self) -> Option<EntityId> {
self.window.next_frame.dispatch_tree.parent_view_id()
}
@@ -3606,7 +3603,7 @@ impl<'a> WindowContext<'a> {
}
/// Updates the IME panel position suggestions for languages like japanese, chinese.
pub fn invalidate_character_coordinates(&mut self) {
pub fn invalidate_character_coordinates(&self) {
self.on_next_frame(|cx| {
if let Some(mut input_handler) = cx.window.platform_window.take_input_handler() {
if let Some(bounds) = input_handler.selected_bounds(cx) {
@@ -3752,7 +3749,7 @@ impl<'a> WindowContext<'a> {
/// Register a callback that can interrupt the closing of the current window based the returned boolean.
/// If the callback returns false, the window won't be closed.
pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
pub fn on_window_should_close(&self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
let mut this = self.to_async();
self.window
.platform_window
@@ -4070,7 +4067,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
/// Sets a given callback to be run on the next frame.
pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
pub fn on_next_frame(&self, f: impl FnOnce(&mut V, &mut ViewContext<V>) + 'static)
where
V: 'static,
{
@@ -4162,7 +4159,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// The callback receives a handle to the view's window. This handle may be
/// invalid, if the window was closed before the view was released.
pub fn on_release(
&mut self,
&self,
on_release: impl FnOnce(&mut V, AnyWindowHandle, &mut AppContext) + 'static,
) -> Subscription {
let window_handle = self.window.handle;
@@ -4179,7 +4176,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// Register a callback to be invoked when the given Model or View is released.
pub fn observe_release<V2, E>(
&mut self,
&self,
entity: &E,
mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, V>) + 'static,
) -> Subscription
@@ -4212,7 +4209,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// Register a callback to be invoked when the window is resized.
pub fn observe_window_bounds(
&mut self,
&self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
@@ -4226,7 +4223,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// Register a callback to be invoked when the window is activated or deactivated.
pub fn observe_window_activation(
&mut self,
&self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
@@ -4240,7 +4237,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// Registers a callback to be invoked when the window appearance changes.
pub fn observe_window_appearance(
&mut self,
&self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
@@ -4260,7 +4257,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
mut f: impl FnMut(&mut V, &KeystrokeEvent, &mut ViewContext<V>) + 'static,
) -> Subscription {
fn inner(
keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>,
keystroke_observers: &SubscriberSet<(), KeystrokeObserver>,
handler: KeystrokeObserver,
) -> Subscription {
let (subscription, activate) = keystroke_observers.insert((), handler);
@@ -4284,7 +4281,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// Register a callback to be invoked when the window's pending input changes.
pub fn observe_pending_input(
&mut self,
&self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
@@ -4372,7 +4369,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// and this callback lets you chose a default place to restore the users focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_lost(
&mut self,
&self,
mut listener: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
@@ -4418,10 +4415,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
/// The given callback is invoked with a [`WeakView<V>`] to avoid leaking the view for a long-running process.
/// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points.
/// The returned future will be polled on the main thread.
pub fn spawn<Fut, R>(
&mut self,
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
) -> Task<R>
pub fn spawn<Fut, R>(&self, f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut) -> Task<R>
where
R: 'static,
Fut: Future<Output = R> + 'static,

View File

@@ -588,7 +588,7 @@ impl IndentGuide {
impl Buffer {
/// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
pub fn local<T: Into<String>>(base_text: T, cx: &ModelContext<Self>) -> Self {
Self::build(
TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()),
None,
@@ -601,7 +601,7 @@ impl Buffer {
pub fn local_normalized(
base_text_normalized: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Self {
Self::build(
TextBuffer::new_normalized(
@@ -934,7 +934,7 @@ impl Buffer {
/// Assign a language registry to the buffer. This allows the buffer to retrieve
/// other languages if parts of the buffer are written in different languages.
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
pub fn set_language_registry(&self, language_registry: Arc<LanguageRegistry>) {
self.syntax_map
.lock()
.set_language_registry(language_registry);
@@ -967,16 +967,13 @@ impl Buffer {
}
/// This method is called to signal that the buffer has been discarded.
pub fn discarded(&mut self, cx: &mut ModelContext<Self>) {
pub fn discarded(&self, cx: &mut ModelContext<Self>) {
cx.emit(BufferEvent::Discarded);
cx.notify();
}
/// Reloads the contents of the buffer from disk.
pub fn reload(
&mut self,
cx: &mut ModelContext<Self>,
) -> oneshot::Receiver<Option<Transaction>> {
pub fn reload(&mut self, cx: &ModelContext<Self>) -> oneshot::Receiver<Option<Transaction>> {
let (tx, rx) = futures::channel::oneshot::channel();
let prev_version = self.text.version();
self.reload_task = Some(cx.spawn(|this, mut cx| async move {
@@ -1085,7 +1082,7 @@ impl Buffer {
/// Sets the text that will be used to compute a Git diff
/// against the buffer text.
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &ModelContext<Self>) {
self.diff_base = diff_base.map(|mut raw_diff_base| {
LineEnding::normalize(&mut raw_diff_base);
BufferDiffBase::Git(Rope::from(raw_diff_base))
@@ -1117,7 +1114,7 @@ impl Buffer {
}
/// Recomputes the diff.
pub fn recalculate_diff(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
pub fn recalculate_diff(&self, cx: &ModelContext<Self>) -> Option<Task<()>> {
let diff_base_rope = match self.diff_base.as_ref()? {
BufferDiffBase::Git(rope) => rope.clone(),
BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(),
@@ -2249,12 +2246,7 @@ impl Buffer {
}
}
fn send_operation(
&mut self,
operation: Operation,
is_local: bool,
cx: &mut ModelContext<Self>,
) {
fn send_operation(&self, operation: Operation, is_local: bool, cx: &mut ModelContext<Self>) {
cx.emit(BufferEvent::Operation {
operation,
is_local,

View File

@@ -22,6 +22,7 @@ pub use request::*;
pub use role::*;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt;
use std::{future::Future, sync::Arc};
use ui::IconName;
@@ -231,6 +232,12 @@ pub struct LanguageModelProviderId(pub SharedString);
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
pub struct LanguageModelProviderName(pub SharedString);
impl fmt::Display for LanguageModelProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for LanguageModelId {
fn from(value: String) -> Self {
Self(SharedString::from(value))

View File

@@ -71,7 +71,7 @@ impl Markdown {
source: String,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>,
cx: &ViewContext<Self>,
fallback_code_block_language: Option<String>,
) -> Self {
let focus_handle = cx.focus_handle();
@@ -97,7 +97,7 @@ impl Markdown {
source: String,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>,
cx: &ViewContext<Self>,
fallback_code_block_language: Option<String>,
) -> Self {
let focus_handle = cx.focus_handle();
@@ -119,12 +119,12 @@ impl Markdown {
this
}
pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) {
pub fn append(&mut self, text: &str, cx: &ViewContext<Self>) {
self.source.push_str(text);
self.parse(cx);
}
pub fn reset(&mut self, source: String, cx: &mut ViewContext<Self>) {
pub fn reset(&mut self, source: String, cx: &ViewContext<Self>) {
if source == self.source() {
return;
}
@@ -145,7 +145,7 @@ impl Markdown {
&self.parsed_markdown
}
fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
fn copy(&self, text: &RenderedText, cx: &ViewContext<Self>) {
if self.selection.end <= self.selection.start {
return;
}
@@ -153,7 +153,7 @@ impl Markdown {
cx.write_to_clipboard(ClipboardItem::new_string(text));
}
fn parse(&mut self, cx: &mut ViewContext<Self>) {
fn parse(&mut self, cx: &ViewContext<Self>) {
if self.source.is_empty() {
return;
}
@@ -319,7 +319,7 @@ impl MarkdownElement {
}
fn paint_selection(
&mut self,
&self,
bounds: Bounds<Pixels>,
rendered_text: &RenderedText,
cx: &mut WindowContext,
@@ -382,7 +382,7 @@ impl MarkdownElement {
}
fn paint_mouse_listeners(
&mut self,
&self,
hitbox: &Hitbox,
rendered_text: &RenderedText,
cx: &mut WindowContext,
@@ -487,7 +487,7 @@ impl MarkdownElement {
});
}
fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
fn autoscroll(&self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
let autoscroll_index = self
.markdown
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;

View File

@@ -515,7 +515,7 @@ impl MultiBuffer {
}
pub fn edit<I, S, T>(
&mut self,
&self,
edits: I,
mut autoindent_mode: Option<AutoindentMode>,
cx: &mut ModelContext<Self>,
@@ -664,7 +664,7 @@ impl MultiBuffer {
drop(snapshot);
// Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
fn tail(
this: &mut MultiBuffer,
this: &MultiBuffer,
buffer_edits: HashMap<BufferId, Vec<BufferEdit>>,
autoindent_mode: Option<AutoindentMode>,
edited_excerpt_ids: Vec<ExcerptId>,
@@ -928,7 +928,7 @@ impl MultiBuffer {
}
}
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext<Self>)
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &ModelContext<Self>)
where
T: IntoIterator<Item = (&'a Model<Buffer>, &'a language::Transaction)>,
{
@@ -952,7 +952,7 @@ impl MultiBuffer {
}
pub fn set_active_selections(
&mut self,
&self,
selections: &[Selection<Anchor>],
line_mode: bool,
cursor_shape: CursorShape,
@@ -1028,7 +1028,7 @@ impl MultiBuffer {
}
}
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
pub fn remove_active_selections(&self, cx: &mut ModelContext<Self>) {
for buffer in self.buffers.borrow().values() {
buffer
.buffer
@@ -1180,7 +1180,7 @@ impl MultiBuffer {
}
pub fn push_multiple_excerpts_with_context_lines(
&mut self,
&self,
buffers_with_ranges: Vec<(Model<Buffer>, Vec<Range<text::Anchor>>)>,
context_line_count: u32,
cx: &mut ModelContext<Self>,
@@ -4218,7 +4218,7 @@ impl History {
&mut self,
buffer_transactions: T,
now: Instant,
cx: &mut ModelContext<MultiBuffer>,
cx: &ModelContext<MultiBuffer>,
) where
T: IntoIterator<Item = (&'a Model<Buffer>, &'a language::Transaction)>,
{

View File

@@ -160,7 +160,7 @@ impl LocalLspStore {
async fn format_locally(
lsp_store: WeakModel<LspStore>,
mut buffers_with_paths: Vec<(Model<Buffer>, Option<PathBuf>)>,
mut buffers: Vec<FormattableBuffer>,
push_to_history: bool,
trigger: FormatTrigger,
mut cx: AsyncAppContext,
@@ -169,22 +169,22 @@ impl LocalLspStore {
// same buffer.
lsp_store.update(&mut cx, |this, cx| {
let this = this.as_local_mut().unwrap();
buffers_with_paths.retain(|(buffer, _)| {
buffers.retain(|buffer| {
this.buffers_being_formatted
.insert(buffer.read(cx).remote_id())
.insert(buffer.handle.read(cx).remote_id())
});
})?;
let _cleanup = defer({
let this = lsp_store.clone();
let mut cx = cx.clone();
let buffers = &buffers_with_paths;
let buffers = &buffers;
move || {
this.update(&mut cx, |this, cx| {
let this = this.as_local_mut().unwrap();
for (buffer, _) in buffers {
for buffer in buffers {
this.buffers_being_formatted
.remove(&buffer.read(cx).remote_id());
.remove(&buffer.handle.read(cx).remote_id());
}
})
.ok();
@@ -192,10 +192,10 @@ impl LocalLspStore {
});
let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path) in &buffers_with_paths {
for buffer in &buffers {
let (primary_adapter_and_server, adapters_and_servers) =
lsp_store.update(&mut cx, |lsp_store, cx| {
let buffer = buffer.read(cx);
let buffer = buffer.handle.read(cx);
let adapters_and_servers = lsp_store
.language_servers_for_buffer(buffer, cx)
@@ -209,7 +209,7 @@ impl LocalLspStore {
(primary_adapter, adapters_and_servers)
})?;
let settings = buffer.update(&mut cx, |buffer, cx| {
let settings = buffer.handle.update(&mut cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx).clone()
})?;
@@ -220,13 +220,14 @@ impl LocalLspStore {
let trailing_whitespace_diff = if remove_trailing_whitespace {
Some(
buffer
.handle
.update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))?
.await,
)
} else {
None
};
let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| {
let whitespace_transaction_id = buffer.handle.update(&mut cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.start_transaction();
if let Some(diff) = trailing_whitespace_diff {
@@ -248,7 +249,7 @@ impl LocalLspStore {
&lsp_store,
&adapters_and_servers,
code_actions,
buffer,
&buffer.handle,
push_to_history,
&mut project_transaction,
&mut cx,
@@ -263,9 +264,9 @@ impl LocalLspStore {
primary_adapter_and_server.map(|(_adapter, server)| server.clone());
let server_and_buffer = primary_language_server
.as_ref()
.zip(buffer_abs_path.as_ref());
.zip(buffer.abs_path.as_ref());
let prettier_settings = buffer.read_with(&cx, |buffer, cx| {
let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx)
.prettier
.clone()
@@ -290,7 +291,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -304,7 +304,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -327,7 +326,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -353,7 +351,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -381,7 +378,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -395,7 +391,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -420,7 +415,6 @@ impl LocalLspStore {
server_and_buffer,
lsp_store.clone(),
buffer,
buffer_abs_path,
&settings,
&adapters_and_servers,
push_to_history,
@@ -440,7 +434,7 @@ impl LocalLspStore {
}
}
buffer.update(&mut cx, |b, cx| {
buffer.handle.update(&mut cx, |b, cx| {
// If the buffer had its whitespace formatted and was edited while the language-specific
// formatting was being computed, avoid applying the language-specific formatting, because
// it can't be grouped with the whitespace formatting in the undo history.
@@ -469,7 +463,7 @@ impl LocalLspStore {
if let Some(transaction_id) = whitespace_transaction_id {
b.group_until_transaction(transaction_id);
} else if let Some(transaction) = project_transaction.0.get(buffer) {
} else if let Some(transaction) = project_transaction.0.get(&buffer.handle) {
b.group_until_transaction(transaction.id)
}
}
@@ -478,7 +472,9 @@ impl LocalLspStore {
if !push_to_history {
b.forget_transaction(transaction.id);
}
project_transaction.0.insert(buffer.clone(), transaction);
project_transaction
.0
.insert(buffer.handle.clone(), transaction);
}
})?;
}
@@ -491,8 +487,7 @@ impl LocalLspStore {
formatter: &Formatter,
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
lsp_store: WeakModel<LspStore>,
buffer: &Model<Buffer>,
buffer_abs_path: &Option<PathBuf>,
buffer: &FormattableBuffer,
settings: &LanguageSettings,
adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
push_to_history: bool,
@@ -516,7 +511,7 @@ impl LocalLspStore {
Some(FormatOperation::Lsp(
LspStore::format_via_lsp(
&lsp_store,
buffer,
&buffer.handle,
buffer_abs_path,
language_server,
settings,
@@ -533,27 +528,20 @@ impl LocalLspStore {
let prettier = lsp_store.update(cx, |lsp_store, _cx| {
lsp_store.prettier_store().unwrap().downgrade()
})?;
prettier_store::format_with_prettier(&prettier, buffer, cx)
prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
.await
.transpose()
.ok()
.flatten()
}
Formatter::External { command, arguments } => {
let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
Self::format_via_external_command(
buffer,
buffer_abs_path,
command,
arguments.as_deref(),
cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
.map(FormatOperation::External)
Self::format_via_external_command(buffer, command, arguments.as_deref(), cx)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
.map(FormatOperation::External)
}
Formatter::CodeActions(code_actions) => {
let code_actions = deserialize_code_actions(code_actions);
@@ -562,7 +550,7 @@ impl LocalLspStore {
&lsp_store,
adapters_and_servers,
code_actions,
buffer,
&buffer.handle,
push_to_history,
transaction,
cx,
@@ -576,13 +564,12 @@ impl LocalLspStore {
}
async fn format_via_external_command(
buffer: &Model<Buffer>,
buffer_abs_path: Option<&Path>,
buffer: &FormattableBuffer,
command: &str,
arguments: Option<&[String]>,
cx: &mut AsyncAppContext,
) -> Result<Option<Diff>> {
let working_dir_path = buffer.update(cx, |buffer, cx| {
let working_dir_path = buffer.handle.update(cx, |buffer, cx| {
let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx);
let mut worktree_path = worktree.abs_path().to_path_buf();
@@ -599,13 +586,17 @@ impl LocalLspStore {
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
}
if let Some(buffer_env) = buffer.env.as_ref() {
child.envs(buffer_env);
}
if let Some(working_dir_path) = working_dir_path {
child.current_dir(working_dir_path);
}
if let Some(arguments) = arguments {
child.args(arguments.iter().map(|arg| {
if let Some(buffer_abs_path) = buffer_abs_path {
if let Some(buffer_abs_path) = buffer.abs_path.as_ref() {
arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
} else {
arg.replace("{buffer_path}", "Untitled")
@@ -623,7 +614,9 @@ impl LocalLspStore {
.stdin
.as_mut()
.ok_or_else(|| anyhow!("failed to acquire stdin"))?;
let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?;
let text = buffer
.handle
.update(cx, |buffer, _| buffer.as_rope().clone())?;
for chunk in text.chunks() {
stdin.write_all(chunk.as_bytes()).await?;
}
@@ -642,12 +635,19 @@ impl LocalLspStore {
let stdout = String::from_utf8(output.stdout)?;
Ok(Some(
buffer
.handle
.update(cx, |buffer, cx| buffer.diff(stdout, cx))?
.await,
))
}
}
pub struct FormattableBuffer {
handle: Model<Buffer>,
abs_path: Option<PathBuf>,
env: Option<HashMap<String, String>>,
}
pub struct RemoteLspStore {
upstream_client: AnyProtoClient,
upstream_project_id: u64,
@@ -5040,6 +5040,28 @@ impl LspStore {
.and_then(|local| local.last_formatting_failure.as_deref())
}
pub fn environment_for_buffer(
&self,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
let worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx));
let worktree_abs_path = worktree_id.and_then(|worktree_id| {
self.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|entry| entry.read(cx).abs_path().clone())
});
if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
environment.update(cx, |env, cx| {
env.get_environment(worktree_id, worktree_abs_path, cx)
})
} else {
Task::ready(None).shared()
}
}
pub fn format(
&mut self,
buffers: HashSet<Model<Buffer>>,
@@ -5054,14 +5076,31 @@ impl LspStore {
let buffer = buffer_handle.read(cx);
let buffer_abs_path = File::from_dyn(buffer.file())
.and_then(|file| file.as_local().map(|f| f.abs_path(cx)));
(buffer_handle, buffer_abs_path)
})
.collect::<Vec<_>>();
cx.spawn(move |lsp_store, mut cx| async move {
let mut formattable_buffers = Vec::with_capacity(buffers_with_paths.len());
for (handle, abs_path) in buffers_with_paths {
let env = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.environment_for_buffer(&handle, cx)
})?
.await;
formattable_buffers.push(FormattableBuffer {
handle,
abs_path,
env,
});
}
let result = LocalLspStore::format_locally(
lsp_store.clone(),
buffers_with_paths,
formattable_buffers,
push_to_history,
trigger,
cx.clone(),

View File

@@ -155,7 +155,7 @@ pub struct Project {
join_project_response_message_id: u32,
user_store: Model<UserStore>,
fs: Arc<dyn Fs>,
ssh_client: Option<Arc<SshRemoteClient>>,
ssh_client: Option<Model<SshRemoteClient>>,
client_state: ProjectClientState,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
@@ -697,7 +697,7 @@ impl Project {
}
pub fn ssh(
ssh: Arc<SshRemoteClient>,
ssh: Model<SshRemoteClient>,
client: Arc<Client>,
node: NodeRuntime,
user_store: Model<UserStore>,
@@ -714,15 +714,16 @@ impl Project {
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
let ssh_proto = ssh.read(cx).to_proto_client();
let worktree_store =
cx.new_model(|_| WorktreeStore::remote(false, ssh.to_proto_client(), 0, None));
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0, None));
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
let buffer_store = cx.new_model(|cx| {
BufferStore::remote(
worktree_store.clone(),
ssh.to_proto_client(),
ssh.read(cx).to_proto_client(),
SSH_PROJECT_ID,
cx,
)
@@ -731,7 +732,7 @@ impl Project {
.detach();
let settings_observer = cx.new_model(|cx| {
SettingsObserver::new_ssh(ssh.to_proto_client(), worktree_store.clone(), cx)
SettingsObserver::new_ssh(ssh_proto.clone(), worktree_store.clone(), cx)
});
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
.detach();
@@ -742,7 +743,7 @@ impl Project {
buffer_store.clone(),
worktree_store.clone(),
languages.clone(),
ssh.to_proto_client(),
ssh_proto.clone(),
SSH_PROJECT_ID,
cx,
)
@@ -750,6 +751,16 @@ impl Project {
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
let dap_store = cx.new_model(DapStore::new);
cx.on_release(|this, cx| {
if let Some(ssh_client) = this.ssh_client.as_ref() {
ssh_client
.read(cx)
.to_proto_client()
.send(proto::ShutdownRemoteServer {})
.log_err();
}
})
.detach();
let this = Self {
buffer_ordered_messages_tx: tx,
@@ -787,20 +798,20 @@ impl Project {
search_excluded_history: Self::new_search_history(),
};
let client: AnyProtoClient = ssh.to_proto_client();
let ssh = ssh.read(cx);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_message_handler(Self::handle_update_project);
client.add_model_request_handler(BufferStore::handle_update_buffer);
BufferStore::init(&client);
LspStore::init(&client);
SettingsObserver::init(&client);
ssh_proto.add_model_message_handler(Self::handle_create_buffer_for_peer);
ssh_proto.add_model_message_handler(Self::handle_update_worktree);
ssh_proto.add_model_message_handler(Self::handle_update_project);
ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer);
BufferStore::init(&ssh_proto);
LspStore::init(&ssh_proto);
SettingsObserver::init(&ssh_proto);
this
})
@@ -1453,7 +1464,7 @@ impl Project {
pub fn ssh_connection_string(&self, cx: &AppContext) -> Option<SharedString> {
if let Some(ssh_state) = &self.ssh_client {
return Some(ssh_state.connection_string().into());
return Some(ssh_state.read(cx).connection_string().into());
}
let dev_server_id = self.dev_server_project_id()?;
dev_server_projects::Store::global(cx)
@@ -1463,8 +1474,8 @@ impl Project {
.clone()
}
pub fn ssh_is_connected(&self) -> Option<bool> {
Some(!self.ssh_client.as_ref()?.is_reconnect_underway())
pub fn ssh_is_connected(&self, cx: &AppContext) -> Option<bool> {
Some(!self.ssh_client.as_ref()?.read(cx).is_reconnect_underway())
}
pub fn replica_id(&self) -> ReplicaId {
@@ -2176,6 +2187,7 @@ impl Project {
BufferStoreEvent::BufferDropped(buffer_id) => {
if let Some(ref ssh_client) = self.ssh_client {
ssh_client
.read(cx)
.to_proto_client()
.send(proto::CloseBuffer {
project_id: 0,
@@ -2406,7 +2418,8 @@ impl Project {
let operation = language::proto::serialize_operation(operation);
if let Some(ssh) = &self.ssh_client {
ssh.to_proto_client()
ssh.read(cx)
.to_proto_client()
.send(proto::UpdateBuffer {
project_id: 0,
buffer_id: buffer_id.to_proto(),
@@ -3093,7 +3106,7 @@ impl Project {
let (tx, rx) = smol::channel::unbounded();
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
(ssh_client.to_proto_client(), 0)
(ssh_client.read(cx).to_proto_client(), 0)
} else if let Some(remote_id) = self.remote_id() {
(self.client.clone().into(), remote_id)
} else {
@@ -3228,12 +3241,14 @@ impl Project {
exists.then(|| ResolvedPath::AbsPath(expanded))
})
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
let request = ssh_client
.to_proto_client()
.request(proto::CheckFileExists {
project_id: SSH_PROJECT_ID,
path: path.to_string(),
});
let request =
ssh_client
.read(cx)
.to_proto_client()
.request(proto::CheckFileExists {
project_id: SSH_PROJECT_ID,
path: path.to_string(),
});
cx.background_executor().spawn(async move {
let response = request.await.log_err()?;
if response.exists {
@@ -3309,7 +3324,7 @@ impl Project {
path: query,
};
let response = session.to_proto_client().request(request);
let response = session.read(cx).to_proto_client().request(request);
cx.background_executor().spawn(async move {
let response = response.await?;
Ok(response.entries.into_iter().map(PathBuf::from).collect())
@@ -3796,7 +3811,7 @@ impl Project {
let mut payload = envelope.payload.clone();
payload.project_id = 0;
cx.background_executor()
.spawn(ssh.to_proto_client().request(payload))
.spawn(ssh.read(cx).to_proto_client().request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.clone()

View File

@@ -321,7 +321,7 @@ impl SettingsObserver {
pub async fn handle_update_user_settings(
_: Model<Self>,
envelope: TypedEnvelope<proto::UpdateUserSettings>,
mut cx: AsyncAppContext,
cx: AsyncAppContext,
) -> anyhow::Result<()> {
cx.update_global(move |settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(&envelope.payload.content, cx)

View File

@@ -70,7 +70,7 @@ impl Project {
if let Some(args) = self
.ssh_client
.as_ref()
.and_then(|session| session.ssh_args())
.and_then(|session| session.read(cx).ssh_args())
{
return Some(SshCommand::Direct(args));
}

View File

@@ -282,7 +282,9 @@ message Envelope {
UpdateUserSettings update_user_settings = 246;
CheckFileExists check_file_exists = 255;
CheckFileExistsResponse check_file_exists_response = 256; // current max
CheckFileExistsResponse check_file_exists_response = 256;
ShutdownRemoteServer shutdown_remote_server = 257; // current max
}
reserved 87 to 88;
@@ -2511,3 +2513,5 @@ message CheckFileExistsResponse {
bool exists = 1;
string path = 2;
}
message ShutdownRemoteServer {}

View File

@@ -364,7 +364,8 @@ messages!(
(CloseBuffer, Foreground),
(UpdateUserSettings, Foreground),
(CheckFileExists, Background),
(CheckFileExistsResponse, Background)
(CheckFileExistsResponse, Background),
(ShutdownRemoteServer, Foreground),
);
request_messages!(
@@ -487,7 +488,8 @@ request_messages!(
(SynchronizeContexts, SynchronizeContextsResponse),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(AddWorktree, AddWorktreeResponse),
(CheckFileExists, CheckFileExistsResponse)
(CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack)
);
entity_messages!(

View File

@@ -22,7 +22,6 @@ futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
log.workspace = true
markdown.workspace = true
menu.workspace = true
ordered-float.workspace = true
picker.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -4,21 +4,21 @@ use anyhow::Result;
use auto_update::AutoUpdater;
use editor::Editor;
use futures::channel::oneshot;
use gpui::AppContext;
use gpui::{
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
Transformation, View,
percentage, px, Action, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext,
DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion,
SharedString, Task, Transformation, View,
};
use gpui::{AppContext, Model};
use release_channel::{AppVersion, ReleaseChannel};
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use ui::{
h_flex, v_flex, Color, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext,
WindowContext,
div, h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, FluentBuilder as _, Icon,
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled,
StyledExt as _, Tooltip, ViewContext, VisualContext, WindowContext,
};
use workspace::{AppState, ModalView, Workspace};
@@ -28,10 +28,6 @@ pub struct SshSettings {
}
impl SshSettings {
pub fn use_direct_ssh(&self) -> bool {
self.ssh_connections.is_some()
}
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> {
self.ssh_connections.clone().into_iter().flatten()
}
@@ -140,47 +136,57 @@ impl SshPrompt {
}
impl Render for SshPrompt {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_full()
.key_context("PasswordPrompt")
.p_4()
.size_full()
.justify_start()
.child(
h_flex()
.gap_2()
.child(if self.error_message.is_some() {
Icon::new(IconName::XCircle)
.size(IconSize::Medium)
.color(Color::Error)
.into_any_element()
} else {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Medium)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element()
})
v_flex()
.p_4()
.size_full()
.child(
Label::new(format!("ssh {}", self.connection_string))
.size(ui::LabelSize::Large),
),
h_flex()
.gap_2()
.justify_between()
.child(h_flex().w_full())
.child(if self.error_message.is_some() {
Icon::new(IconName::XCircle)
.size(IconSize::Medium)
.color(Color::Error)
.into_any_element()
} else {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Medium)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
)
.into_any_element()
})
.child(Label::new(format!(
"Connecting to {}",
self.connection_string
)))
.child(h_flex().w_full()),
)
.when_some(self.error_message.as_ref(), |el, error| {
el.child(Label::new(error.clone()))
})
.when(
self.error_message.is_none() && self.status_message.is_some(),
|el| el.child(Label::new(self.status_message.clone().unwrap())),
)
.when_some(self.prompt.as_ref(), |el, prompt| {
el.child(Label::new(prompt.0.clone()))
.child(self.editor.clone())
}),
)
.when_some(self.error_message.as_ref(), |el, error| {
el.child(Label::new(error.clone()))
})
.when(
self.error_message.is_none() && self.status_message.is_some(),
|el| el.child(Label::new(self.status_message.clone().unwrap())),
)
.when_some(self.prompt.as_ref(), |el, prompt| {
el.child(Label::new(prompt.0.clone()))
.child(self.editor.clone())
})
}
}
@@ -202,14 +208,41 @@ impl SshConnectionModal {
impl Render for SshConnectionModal {
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
let connection_string = self.prompt.read(cx).connection_string.clone();
let theme = cx.theme();
let header_color = theme.colors().element_background;
let body_color = theme.colors().background;
v_flex()
.elevation_3(cx)
.p_4()
.gap_2()
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.w(px(400.))
.child(self.prompt.clone())
.child(
h_flex()
.p_1()
.border_b_1()
.border_color(theme.colors().border)
.bg(header_color)
.justify_between()
.child(
IconButton::new("ssh-connection-cancel", IconName::ArrowLeft)
.icon_size(IconSize::XSmall)
.on_click(|_, cx| cx.dispatch_action(menu::Cancel.boxed_clone()))
.tooltip(|cx| Tooltip::for_action("Back", &menu::Cancel, cx)),
)
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::Server).size(IconSize::XSmall))
.child(
Label::new(connection_string)
.size(ui::LabelSize::Small)
.single_line(),
),
)
.child(div()),
)
.child(h_flex().bg(body_color).w_full().child(self.prompt.clone()))
}
}
@@ -373,25 +406,24 @@ impl SshClientDelegate {
}
pub fn connect_over_ssh(
unique_identifier: String,
connection_options: SshConnectionOptions,
ui: View<SshPrompt>,
cx: &mut WindowContext,
) -> Task<Result<Arc<SshRemoteClient>>> {
) -> Task<Result<Model<SshRemoteClient>>> {
let window = cx.window_handle();
let known_password = connection_options.password.clone();
cx.spawn(|mut cx| async move {
remote::SshRemoteClient::new(
connection_options,
Arc::new(SshClientDelegate {
window,
ui,
known_password,
}),
&mut cx,
)
.await
})
remote::SshRemoteClient::new(
unique_identifier,
connection_options,
Arc::new(SshClientDelegate {
window,
ui,
known_password,
}),
cx,
)
}
pub async fn open_ssh_project(
@@ -420,22 +452,25 @@ pub async fn open_ssh_project(
})?
};
let session = window
.update(cx, |workspace, cx| {
cx.activate_window();
workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
let ui = workspace
.active_modal::<SshConnectionModal>(cx)
.unwrap()
.read(cx)
.prompt
.clone();
connect_over_ssh(connection_options.clone(), ui, cx)
})?
.await?;
let delegate = window.update(cx, |workspace, cx| {
cx.activate_window();
workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
let ui = workspace
.active_modal::<SshConnectionModal>(cx)
.unwrap()
.read(cx)
.prompt
.clone();
Arc::new(SshClientDelegate {
window: cx.window_handle(),
ui,
known_password: connection_options.password.clone(),
})
})?;
cx.update(|cx| {
workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx)
workspace::open_ssh_project(window, connection_options, delegate, app_state, paths, cx)
})?
.await
}

View File

@@ -49,3 +49,17 @@ pub async fn write_message<S: AsyncWrite + Unpin>(
stream.write_all(buffer).await?;
Ok(())
}
pub async fn read_message_raw<S: AsyncRead + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
buffer.resize(MESSAGE_LEN_SIZE, 0);
stream.read_exact(buffer).await?;
let message_len = message_len_from_buffer(buffer);
buffer.resize(message_len as usize, 0);
stream.read_exact(buffer).await?;
Ok(())
}

View File

@@ -15,7 +15,9 @@ use futures::{
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
StreamExt as _,
};
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task};
use gpui::{
AppContext, AsyncAppContext, Context, Model, ModelContext, SemanticVersion, Task, WeakModel,
};
use parking_lot::Mutex;
use rpc::{
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
@@ -24,16 +26,18 @@ use rpc::{
use smol::{
fs,
process::{self, Child, Stdio},
Timer,
};
use std::{
any::TypeId,
ffi::OsStr,
mem,
path::{Path, PathBuf},
sync::{
atomic::{AtomicU32, Ordering::SeqCst},
Arc, Weak,
Arc,
},
time::Instant,
time::{Duration, Instant},
};
use tempfile::TempDir;
use util::maybe;
@@ -92,6 +96,17 @@ impl SshConnectionOptions {
host
}
}
// Uniquely identifies dev server projects on a remote host. Needs to be
// stable for the same dev server project.
pub fn dev_server_identifier(&self) -> String {
let mut identifier = format!("dev-server-{:?}", self.host);
if let Some(username) = self.username.as_ref() {
identifier.push('-');
identifier.push_str(&username);
}
identifier
}
}
#[derive(Copy, Clone, Debug)]
@@ -156,28 +171,6 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
))
}
}
#[cfg(unix)]
async fn read_with_timeout(
stdout: &mut process::ChildStdout,
timeout: std::time::Duration,
output: &mut Vec<u8>,
) -> Result<(), std::io::Error> {
smol::future::or(
async {
stdout.read_to_end(output).await?;
Ok::<_, std::io::Error>(())
},
async {
smol::Timer::after(timeout).await;
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Read operation timed out",
))
},
)
.await
}
struct ChannelForwarder {
quit_tx: UnboundedSender<()>,
@@ -188,7 +181,7 @@ impl ChannelForwarder {
fn new(
mut incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
cx: &mut AsyncAppContext,
cx: &AsyncAppContext,
) -> (Self, UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
let (quit_tx, mut quit_rx) = mpsc::unbounded::<()>();
@@ -246,72 +239,119 @@ struct SshRemoteClientState {
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
multiplex_task: Task<Result<()>>,
heartbeat_task: Task<Result<()>>,
}
pub struct SshRemoteClient {
client: Arc<ChannelClient>,
inner_state: Mutex<Option<SshRemoteClientState>>,
unique_identifier: String,
connection_options: SshConnectionOptions,
inner_state: Arc<Mutex<Option<SshRemoteClientState>>>,
}
impl Drop for SshRemoteClient {
fn drop(&mut self) {
self.shutdown_processes();
}
}
impl SshRemoteClient {
pub async fn new(
pub fn new(
unique_identifier: String,
connection_options: SshConnectionOptions,
delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Arc<Self>> {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
cx: &AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
let this = Arc::new(Self {
client,
inner_state: Mutex::new(None),
connection_options: connection_options.clone(),
});
let this = cx.new_model(|cx| {
cx.on_app_quit(|this: &mut Self, _| {
this.shutdown_processes();
futures::future::ready(())
})
.detach();
let inner_state = {
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, cx);
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx);
Self {
client,
unique_identifier: unique_identifier.clone(),
connection_options: SshConnectionOptions::default(),
inner_state: Arc::new(Mutex::new(None)),
}
})?;
let (ssh_connection, ssh_process) =
Self::establish_connection(connection_options, delegate.clone(), cx).await?;
let inner_state = {
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let multiplex_task = Self::multiplex(
Arc::downgrade(&this),
ssh_process,
proxy_incoming_tx,
proxy_outgoing_rx,
cx,
);
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
unique_identifier,
connection_options,
delegate.clone(),
&mut cx,
)
.await?;
SshRemoteClientState {
ssh_connection,
delegate,
forwarder: proxy,
multiplex_task,
}
};
let multiplex_task = Self::multiplex(
this.downgrade(),
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
&mut cx,
);
this.inner_state.lock().replace(inner_state);
SshRemoteClientState {
ssh_connection,
delegate,
forwarder: proxy,
multiplex_task,
heartbeat_task: Self::heartbeat(this.downgrade(), &mut cx),
}
};
Ok(this)
this.update(&mut cx, |this, cx| {
this.inner_state.lock().replace(inner_state);
cx.notify();
})?;
Ok(this)
})
}
fn reconnect(this: Arc<Self>, cx: &mut AsyncAppContext) -> Result<()> {
let Some(state) = this.inner_state.lock().take() else {
fn shutdown_processes(&self) {
let Some(mut state) = self.inner_state.lock().take() else {
return;
};
log::info!("shutting down ssh processes");
// Drop `multiplex_task` because it owns our ssh_proxy_process, which is a
// child of master_process.
let task = mem::replace(&mut state.multiplex_task, Task::ready(Ok(())));
drop(task);
// Now drop the rest of state, which kills master process.
drop(state);
}
fn reconnect(&self, cx: &ModelContext<Self>) -> Result<()> {
log::info!("Trying to reconnect to ssh server...");
let Some(state) = self.inner_state.lock().take() else {
return Err(anyhow!("reconnect is already in progress"));
};
let workspace_identifier = self.unique_identifier.clone();
let SshRemoteClientState {
mut ssh_connection,
delegate,
forwarder: proxy,
multiplex_task,
heartbeat_task,
} = state;
drop(multiplex_task);
drop(heartbeat_task);
cx.spawn(|mut cx| async move {
cx.spawn(|this, mut cx| async move {
let (incoming_tx, outgoing_rx) = proxy.into_channels().await;
ssh_connection.master_process.kill()?;
@@ -323,8 +363,13 @@ impl SshRemoteClient {
let connection_options = ssh_connection.socket.connection_options.clone();
let (ssh_connection, ssh_process) =
Self::establish_connection(connection_options, delegate.clone(), &mut cx).await?;
let (ssh_connection, ssh_process) = Self::establish_connection(
workspace_identifier,
connection_options,
delegate.clone(),
&mut cx,
)
.await?;
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
@@ -334,32 +379,95 @@ impl SshRemoteClient {
delegate,
forwarder: proxy,
multiplex_task: Self::multiplex(
Arc::downgrade(&this),
this.clone(),
ssh_process,
proxy_incoming_tx,
proxy_outgoing_rx,
&mut cx,
),
heartbeat_task: Self::heartbeat(this.clone(), &mut cx),
};
this.inner_state.lock().replace(inner_state);
anyhow::Ok(())
this.update(&mut cx, |this, _| {
this.inner_state.lock().replace(inner_state);
})
})
.detach();
Ok(())
}
anyhow::Ok(())
fn heartbeat(this: WeakModel<Self>, cx: &mut AsyncAppContext) -> Task<Result<()>> {
let Ok(client) = this.update(cx, |this, _| this.client.clone()) else {
return Task::ready(Err(anyhow!("SshRemoteClient lost")));
};
cx.spawn(|mut cx| {
let this = this.clone();
async move {
const MAX_MISSED_HEARTBEATS: usize = 5;
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
let mut missed_heartbeats = 0;
let mut timer = Timer::interval(HEARTBEAT_INTERVAL);
loop {
timer.next().await;
log::info!("Sending heartbeat to server...");
let result = smol::future::or(
async {
client.request(proto::Ping {}).await?;
Ok(())
},
async {
smol::Timer::after(HEARTBEAT_TIMEOUT).await;
Err(anyhow!("Timeout detected"))
},
)
.await;
if result.is_err() {
missed_heartbeats += 1;
log::warn!(
"No heartbeat from server after {:?}. Missed heartbeat {} out of {}.",
HEARTBEAT_TIMEOUT,
missed_heartbeats,
MAX_MISSED_HEARTBEATS
);
} else {
missed_heartbeats = 0;
}
if missed_heartbeats >= MAX_MISSED_HEARTBEATS {
log::error!(
"Missed last {} hearbeats. Reconnecting...",
missed_heartbeats
);
this.update(&mut cx, |this, cx| {
this.reconnect(cx)
.context("failed to reconnect after missing heartbeats")
})
.context("failed to update weak reference, SshRemoteClient lost?")??;
return Ok(());
}
}
}
})
}
fn multiplex(
this: Weak<Self>,
mut ssh_process: Child,
this: WeakModel<Self>,
mut ssh_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
cx: &mut AsyncAppContext,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
let mut child_stderr = ssh_process.stderr.take().unwrap();
let mut child_stdout = ssh_process.stdout.take().unwrap();
let mut child_stdin = ssh_process.stdin.take().unwrap();
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
let io_task = cx.background_executor().spawn(async move {
let mut stdin_buffer = Vec::new();
@@ -385,7 +493,7 @@ impl SshRemoteClient {
Ok(0) => {
child_stdin.close().await?;
outgoing_rx.close();
let status = ssh_process.status().await?;
let status = ssh_proxy_process.status().await?;
if !status.success() {
log::error!("ssh process exited with status: {status:?}");
return Err(anyhow!("ssh process exited with non-zero status code: {:?}", status.code()));
@@ -446,9 +554,9 @@ impl SshRemoteClient {
if let Err(error) = result {
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
if let Some(this) = this.upgrade() {
Self::reconnect(this, &mut cx).ok();
}
this.update(&mut cx, |this, cx| {
this.reconnect(cx).ok();
})?;
}
Ok(())
@@ -456,6 +564,7 @@ impl SshRemoteClient {
}
async fn establish_connection(
unique_identifier: String,
connection_options: SshConnectionOptions,
delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
@@ -479,17 +588,22 @@ impl SshRemoteClient {
let socket = ssh_connection.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
let ssh_process = socket
delegate.set_status(Some("Starting proxy"), cx);
let ssh_proxy_process = socket
.ssh_command(format!(
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
"RUST_LOG={} RUST_BACKTRACE={} {:?} proxy --identifier {}",
std::env::var("RUST_LOG").unwrap_or_default(),
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
remote_binary_path,
unique_identifier,
))
// IMPORTANT: we kill this process when we drop the task that uses it.
.kill_on_drop(true)
.spawn()
.context("failed to spawn remote server")?;
Ok((ssh_connection, ssh_process))
Ok((ssh_connection, ssh_proxy_process))
}
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
@@ -514,21 +628,25 @@ impl SshRemoteClient {
pub fn is_reconnect_underway(&self) -> bool {
maybe!({ Some(self.inner_state.try_lock()?.is_none()) }).unwrap_or_default()
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(
client_cx: &mut gpui::TestAppContext,
server_cx: &mut gpui::TestAppContext,
) -> (Arc<Self>, Arc<ChannelClient>) {
) -> (Model<Self>, Arc<ChannelClient>) {
use gpui::Context;
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
(
client_cx.update(|cx| {
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
Arc::new(Self {
cx.new_model(|_| Self {
client,
inner_state: Mutex::new(None),
unique_identifier: "fake".to_string(),
connection_options: SshConnectionOptions::default(),
inner_state: Arc::new(Mutex::new(None)),
})
}),
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
@@ -585,13 +703,19 @@ impl SshRemoteConnection {
// Create a domain socket listener to handle requests from the askpass program.
let askpass_socket = temp_dir.path().join("askpass.sock");
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let askpass_task = cx.spawn({
let delegate = delegate.clone();
|mut cx| async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
while let Ok((mut stream, _)) = listener.accept().await {
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
askpass_opened_tx.send(()).ok();
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
@@ -641,20 +765,29 @@ impl SshRemoteConnection {
// has completed.
let stdout = master_process.stdout.as_mut().unwrap();
let mut output = Vec::new();
let connection_timeout = std::time::Duration::from_secs(10);
let result = read_with_timeout(stdout, connection_timeout, &mut output).await;
if let Err(e) = result {
let error_message = if e.kind() == std::io::ErrorKind::TimedOut {
format!(
"Failed to connect to host. Timed out after {:?}.",
connection_timeout
)
} else {
format!("Failed to connect to host: {}.", e)
};
let connection_timeout = Duration::from_secs(10);
let result = select_biased! {
_ = askpass_opened_rx.fuse() => {
// If the askpass script has opened, that means the user is typing
// their password, in which case we don't want to timeout anymore,
// since we know a connection has been established.
stdout.read_to_end(&mut output).await?;
Ok(())
}
result = stdout.read_to_end(&mut output).fuse() => {
result?;
Ok(())
}
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
Err(anyhow!("Exceeded {:?} timeout trying to connect to host", connection_timeout))
}
};
if let Err(e) = result {
let error_message = format!("Failed to connect to host: {}.", e);
delegate.set_error(error_message, cx);
return Err(e.into());
return Err(e);
}
drop(askpass_task);
@@ -663,10 +796,10 @@ impl SshRemoteConnection {
output.clear();
let mut stderr = master_process.stderr.take().unwrap();
stderr.read_to_end(&mut output).await?;
Err(anyhow!(
"failed to connect: {}",
String::from_utf8_lossy(&output)
))?;
let error_message = format!("failed to connect: {}", String::from_utf8_lossy(&output));
delegate.set_error(error_message.clone(), cx);
Err(anyhow!(error_message))?;
}
Ok(Self {

View File

@@ -22,25 +22,26 @@ test-support = ["fs/test-support"]
[dependencies]
anyhow.workspace = true
clap.workspace = true
client.workspace = true
env_logger.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
node_runtime.workspace = true
language.workspace = true
languages.workspace = true
log.workspace = true
node_runtime.workspace = true
project.workspace = true
remote.workspace = true
rpc.workspace = true
settings.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
smol.workspace = true
worktree.workspace = true
language.workspace = true
languages.workspace = true
util.workspace = true
worktree.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -117,6 +117,8 @@ impl HeadlessProject {
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists);
client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server);
client.add_request_handler(cx.weak_model(), Self::handle_ping);
client.add_model_request_handler(Self::handle_add_worktree);
client.add_model_request_handler(Self::handle_open_buffer_by_path);
@@ -340,4 +342,31 @@ impl HeadlessProject {
path: expanded,
})
}
pub async fn handle_shutdown_remote_server(
_this: Model<Self>,
_envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
cx: AsyncAppContext,
) -> Result<proto::Ack> {
cx.spawn(|cx| async move {
cx.update(|cx| {
// TODO: This is a hack, because in a headless project, shutdown isn't executed
// when calling quit, but it should be.
cx.shutdown();
cx.quit();
})
})
.detach();
Ok(proto::Ack {})
}
pub async fn handle_ping(
_this: Model<Self>,
_envelope: TypedEnvelope<proto::Ping>,
_cx: AsyncAppContext,
) -> Result<proto::Ack> {
log::debug!("Received ping from client");
Ok(proto::Ack {})
}
}

View File

@@ -1,20 +1,34 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
use fs::RealFs;
use futures::channel::mpsc;
use gpui::Context as _;
use remote::{
json_log::LogRecord,
protocol::{read_message, write_message},
};
use remote_server::HeadlessProject;
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
use std::{
env,
io::{self, Write},
mem, process,
sync::Arc,
};
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(disable_version_flag = true)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(long)]
log_file: PathBuf,
#[arg(long)]
pid_file: PathBuf,
#[arg(long)]
stdin_socket: PathBuf,
#[arg(long)]
stdout_socket: PathBuf,
},
Proxy {
#[arg(long)]
identifier: String,
},
Version,
}
#[cfg(windows)]
fn main() {
@@ -22,76 +36,32 @@ fn main() {
}
#[cfg(not(windows))]
fn main() {
use remote::ssh_session::ChannelClient;
fn main() -> Result<()> {
use remote_server::unix::{execute_proxy, execute_run, init_logging};
env_logger::builder()
.format(|buf, record| {
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
buf.write_all(b"\n")?;
Ok(())
})
.init();
let cli = Cli::parse();
let subcommand = std::env::args().nth(1);
match subcommand.as_deref() {
Some("run") => {}
Some("version") => {
println!("{}", env!("ZED_PKG_VERSION"));
return;
match cli.command {
Some(Commands::Run {
log_file,
pid_file,
stdin_socket,
stdout_socket,
}) => {
init_logging(Some(log_file))?;
execute_run(pid_file, stdin_socket, stdout_socket)
}
_ => {
eprintln!("usage: remote <run|version>");
process::exit(1);
Some(Commands::Proxy { identifier }) => {
init_logging(None)?;
execute_proxy(identifier)
}
Some(Commands::Version) => {
eprintln!("{}", env!("ZED_PKG_VERSION"));
Ok(())
}
None => {
eprintln!("usage: remote <run|proxy|version>");
std::process::exit(1);
}
}
gpui::App::headless().run(move |cx| {
settings::init(cx);
HeadlessProject::init(cx);
let (incoming_tx, incoming_rx) = mpsc::unbounded();
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
let mut stdin = Async::new(io::stdin()).unwrap();
let mut stdout = Async::new(io::stdout()).unwrap();
let session = ChannelClient::new(incoming_rx, outgoing_tx, cx);
let project = cx.new_model(|cx| {
HeadlessProject::new(
session.clone(),
Arc::new(RealFs::new(Default::default(), None)),
cx,
)
});
cx.background_executor()
.spawn(async move {
let mut output_buffer = Vec::new();
while let Some(message) = outgoing_rx.next().await {
write_message(&mut stdout, &mut output_buffer, message).await?;
stdout.flush().await?;
}
anyhow::Ok(())
})
.detach();
cx.background_executor()
.spawn(async move {
let mut input_buffer = Vec::new();
loop {
let message = match read_message(&mut stdin, &mut input_buffer).await {
Ok(message) => message,
Err(error) => {
log::warn!("error reading message: {:?}", error);
process::exit(0);
}
};
incoming_tx.unbounded_send(message).ok();
}
})
.detach();
mem::forget(project);
});
}

View File

@@ -655,7 +655,7 @@ async fn init_test(
(project, headless, fs)
}
fn build_project(ssh: Arc<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);

View File

@@ -1,5 +1,8 @@
mod headless_project;
#[cfg(not(windows))]
pub mod unix;
#[cfg(test)]
mod remote_editing_tests;

View File

@@ -0,0 +1,336 @@
use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result};
use fs::RealFs;
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, FutureExt, SinkExt};
use gpui::{AppContext, Context as _};
use remote::ssh_session::ChannelClient;
use remote::{
json_log::LogRecord,
protocol::{read_message, write_message},
};
use rpc::proto::Envelope;
use smol::Async;
use smol::{io::AsyncWriteExt, net::unix::UnixListener, stream::StreamExt as _};
use std::{
env,
io::Write,
mem,
path::{Path, PathBuf},
sync::Arc,
};
pub fn init_logging(log_file: Option<PathBuf>) -> Result<()> {
if let Some(log_file) = log_file {
let target = Box::new(if log_file.exists() {
std::fs::OpenOptions::new()
.append(true)
.open(&log_file)
.context("Failed to open log file in append mode")?
} else {
std::fs::File::create(&log_file).context("Failed to create log file")?
});
env_logger::Builder::from_default_env()
.target(env_logger::Target::Pipe(target))
.init();
} else {
env_logger::builder()
.format(|buf, record| {
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
buf.write_all(b"\n")?;
Ok(())
})
.init();
}
Ok(())
}
fn start_server(
stdin_listener: UnixListener,
stdout_listener: UnixListener,
cx: &mut AppContext,
) -> Arc<ChannelClient> {
// This is the server idle timeout. If no connection comes in in this timeout, the server will shut down.
const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60);
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
let (app_quit_tx, mut app_quit_rx) = mpsc::unbounded::<()>();
cx.on_app_quit(move |_| {
let mut app_quit_tx = app_quit_tx.clone();
async move {
app_quit_tx.send(()).await.ok();
}
})
.detach();
cx.spawn(|cx| async move {
let mut stdin_incoming = stdin_listener.incoming();
let mut stdout_incoming = stdout_listener.incoming();
loop {
let streams = futures::future::join(stdin_incoming.next(), stdout_incoming.next());
log::info!("server: accepting new connections");
let result = select! {
streams = streams.fuse() => {
let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream))) = streams else {
break;
};
anyhow::Ok((stdin_stream, stdout_stream))
}
_ = futures::FutureExt::fuse(smol::Timer::after(IDLE_TIMEOUT)) => {
log::warn!("server: timed out waiting for new connections after {:?}. exiting.", IDLE_TIMEOUT);
cx.update(|cx| {
// TODO: This is a hack, because in a headless project, shutdown isn't executed
// when calling quit, but it should be.
cx.shutdown();
cx.quit();
})?;
break;
}
_ = app_quit_rx.next().fuse() => {
break;
}
};
let Ok((mut stdin_stream, mut stdout_stream)) = result else {
break;
};
let mut input_buffer = Vec::new();
let mut output_buffer = Vec::new();
loop {
select_biased! {
_ = app_quit_rx.next().fuse() => {
return anyhow::Ok(());
}
stdin_message = read_message(&mut stdin_stream, &mut input_buffer).fuse() => {
let message = match stdin_message {
Ok(message) => message,
Err(error) => {
log::warn!("server: error reading message on stdin: {}. exiting.", error);
break;
}
};
if let Err(error) = incoming_tx.unbounded_send(message) {
log::error!("server: failed to send message to application: {:?}. exiting.", error);
return Err(anyhow!(error));
}
}
outgoing_message = outgoing_rx.next().fuse() => {
let Some(message) = outgoing_message else {
log::error!("server: stdout handler, no message");
break;
};
if let Err(error) =
write_message(&mut stdout_stream, &mut output_buffer, message).await
{
log::error!("server: failed to write stdout message: {:?}", error);
break;
}
if let Err(error) = stdout_stream.flush().await {
log::error!("server: failed to flush stdout message: {:?}", error);
break;
}
}
}
}
}
anyhow::Ok(())
})
.detach();
ChannelClient::new(incoming_rx, outgoing_tx, cx)
}
pub fn execute_run(pid_file: PathBuf, stdin_socket: PathBuf, stdout_socket: PathBuf) -> Result<()> {
write_pid_file(&pid_file)
.with_context(|| format!("failed to write pid file: {:?}", &pid_file))?;
let stdin_listener = UnixListener::bind(stdin_socket).context("failed to bind stdin socket")?;
let stdout_listener =
UnixListener::bind(stdout_socket).context("failed to bind stdout socket")?;
gpui::App::headless().run(move |cx| {
settings::init(cx);
HeadlessProject::init(cx);
let session = start_server(stdin_listener, stdout_listener, cx);
let project = cx.new_model(|cx| {
HeadlessProject::new(session, Arc::new(RealFs::new(Default::default(), None)), cx)
});
mem::forget(project);
});
log::info!("server: gpui app is shut down. quitting.");
Ok(())
}
pub fn execute_proxy(identifier: String) -> Result<()> {
log::debug!("proxy: starting up. PID: {}", std::process::id());
let project_dir = ensure_project_dir(&identifier)?;
let pid_file = project_dir.join("server.pid");
let stdin_socket = project_dir.join("stdin.sock");
let stdout_socket = project_dir.join("stdout.sock");
let log_file = project_dir.join("server.log");
let server_running = check_pid_file(&pid_file)?;
if !server_running {
spawn_server(&log_file, &pid_file, &stdin_socket, &stdout_socket)?;
};
let stdin_task = smol::spawn(async move {
let stdin = Async::new(std::io::stdin())?;
let stream = smol::net::unix::UnixStream::connect(stdin_socket).await?;
handle_io(stdin, stream, "stdin").await
});
let stdout_task: smol::Task<Result<()>> = smol::spawn(async move {
let stdout = Async::new(std::io::stdout())?;
let stream = smol::net::unix::UnixStream::connect(stdout_socket).await?;
handle_io(stream, stdout, "stdout").await
});
if let Err(forwarding_result) =
smol::block_on(async move { smol::future::race(stdin_task, stdout_task).await })
{
log::error!(
"proxy: failed to forward messages: {:?}, terminating...",
forwarding_result
);
return Err(forwarding_result);
}
Ok(())
}
fn ensure_project_dir(identifier: &str) -> Result<PathBuf> {
let project_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let project_dir = PathBuf::from(project_dir)
.join(".local")
.join("state")
.join("zed-remote-server")
.join(identifier);
std::fs::create_dir_all(&project_dir)?;
Ok(project_dir)
}
fn spawn_server(
log_file: &Path,
pid_file: &Path,
stdin_socket: &Path,
stdout_socket: &Path,
) -> Result<()> {
if stdin_socket.exists() {
std::fs::remove_file(&stdin_socket)?;
}
if stdout_socket.exists() {
std::fs::remove_file(&stdout_socket)?;
}
let binary_name = std::env::current_exe()?;
let server_process = std::process::Command::new(binary_name)
.arg("run")
.arg("--log-file")
.arg(log_file)
.arg("--pid-file")
.arg(pid_file)
.arg("--stdin-socket")
.arg(stdin_socket)
.arg("--stdout-socket")
.arg(stdout_socket)
.spawn()?;
log::debug!("proxy: server started. PID: {:?}", server_process.id());
let mut total_time_waited = std::time::Duration::from_secs(0);
let wait_duration = std::time::Duration::from_millis(20);
while !stdout_socket.exists() || !stdin_socket.exists() {
log::debug!("proxy: waiting for server to be ready to accept connections...");
std::thread::sleep(wait_duration);
total_time_waited += wait_duration;
}
log::info!(
"proxy: server ready to accept connections. total time waited: {:?}",
total_time_waited
);
Ok(())
}
fn check_pid_file(path: &Path) -> Result<bool> {
let Some(pid) = std::fs::read_to_string(&path)
.ok()
.and_then(|contents| contents.parse::<u32>().ok())
else {
return Ok(false);
};
log::debug!("proxy: Checking if process with PID {} exists...", pid);
match std::process::Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.output()
{
Ok(output) if output.status.success() => {
log::debug!("proxy: Process with PID {} exists. NOT spawning new server, but attaching to existing one.", pid);
Ok(true)
}
_ => {
log::debug!("proxy: Found PID file, but process with that PID does not exist. Removing PID file.");
std::fs::remove_file(&path).context("proxy: Failed to remove PID file")?;
Ok(false)
}
}
}
fn write_pid_file(path: &Path) -> Result<()> {
if path.exists() {
std::fs::remove_file(path)?;
}
std::fs::write(path, std::process::id().to_string()).context("Failed to write PID file")
}
async fn handle_io<R, W>(mut reader: R, mut writer: W, socket_name: &str) -> Result<()>
where
R: AsyncRead + Unpin,
W: AsyncWrite + Unpin,
{
use remote::protocol::read_message_raw;
let mut buffer = Vec::new();
loop {
read_message_raw(&mut reader, &mut buffer)
.await
.with_context(|| format!("proxy: failed to read message from {}", socket_name))?;
write_size_prefixed_buffer(&mut writer, &mut buffer)
.await
.with_context(|| format!("proxy: failed to write message to {}", socket_name))?;
writer.flush().await?;
buffer.clear();
}
}
async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream: &mut S,
buffer: &mut Vec<u8>,
) -> Result<()> {
let len = buffer.len() as u32;
stream.write_all(len.to_le_bytes().as_slice()).await?;
stream.write_all(buffer).await?;
Ok(())
}

View File

@@ -64,7 +64,7 @@ pub struct AppSession {
}
impl AppSession {
pub fn new(session: Session, cx: &mut ModelContext<Self>) -> Self {
pub fn new(session: Session, cx: &ModelContext<Self>) -> Self {
let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)];
let _serialization_task = Some(cx.spawn(|_, cx| async move {

View File

@@ -77,7 +77,7 @@ pub fn handle_settings_file_changes(
.set_user_settings(&user_settings_content, cx)
.log_err();
});
cx.spawn(move |mut cx| async move {
cx.spawn(move |cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await {
let result = cx.update_global(|store: &mut SettingsStore, cx| {
let result = store.set_user_settings(&user_settings_content, cx);

View File

@@ -179,7 +179,7 @@ impl SnippetProvider {
}
/// Add directory to be watched for content changes
fn watch_directory(&mut self, path: &Path, cx: &mut ModelContext<Self>) {
fn watch_directory(&mut self, path: &Path, cx: &ModelContext<Self>) {
let path: Arc<Path> = Arc::from(path);
self.watch_tasks.push(cx.spawn(|this, mut cx| async move {

View File

@@ -17,6 +17,7 @@ libsqlite3-sys = { version = "0.28", features = ["bundled"] }
parking_lot.workspace = true
project.workspace = true
smol.workspace = true
sqlformat.workspace = true
thread_local = "1.1.4"
util.workspace = true
uuid.workspace = true

View File

@@ -55,7 +55,16 @@ impl Connection {
.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
for (index, migration) in migrations.iter().enumerate() {
let migration =
sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default());
if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
// Reformat completed migrations with the current `sqlformat` version, so that past migrations stored
// conform to the new formatting rules.
let completed_migration = sqlformat::format(
completed_migration,
&sqlformat::QueryParams::None,
Default::default(),
);
if completed_migration == migration {
// Migration already run. Continue
continue;
@@ -71,8 +80,8 @@ impl Connection {
}
}
self.eager_exec(migration)?;
store_completed_migration((domain, index, *migration))?;
self.eager_exec(&migration)?;
store_completed_migration((domain, index, migration))?;
}
Ok(())
@@ -108,11 +117,7 @@ mod test {
.select::<String>("SELECT (migration) FROM migrations")
.unwrap()()
.unwrap()[..],
&[indoc! {"
CREATE TABLE test1 (
a TEXT,
b TEXT
)"}],
&[indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}],
);
// Add another step to the migration and run it again
@@ -141,16 +146,8 @@ mod test {
.unwrap()()
.unwrap()[..],
&[
indoc! {"
CREATE TABLE test1 (
a TEXT,
b TEXT
)"},
indoc! {"
CREATE TABLE test2 (
c TEXT,
d TEXT
)"},
indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"},
indoc! {"CREATE TABLE test2 (c TEXT, d TEXT)"},
],
);
}

View File

@@ -15,5 +15,5 @@ doctest = false
[dependencies]
sqlez.workspace = true
sqlformat = "0.2"
sqlformat.workspace = true
syn = "1.0"

View File

@@ -35,6 +35,7 @@ strum = { workspace = true, features = ["derive"] }
theme.workspace = true
title_bar = { workspace = true, features = ["stories"] }
ui = { workspace = true, features = ["stories"] }
ureq_client.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -1,7 +1,7 @@
use gpui::{AnyElement, Hsla, Render};
use story::Story;
use ui::{prelude::*, WithRemSize};
use ui::{prelude::*, utils::WithRemSize};
pub struct WithRemSizeStory;

View File

@@ -4,6 +4,8 @@ mod assets;
mod stories;
mod story_selector;
use std::sync::Arc;
use clap::Parser;
use dialoguer::FuzzySelect;
use gpui::{
@@ -17,6 +19,7 @@ use simplelog::SimpleLogger;
use strum::IntoEnumIterator;
use theme::{ThemeRegistry, ThemeSettings};
use ui::prelude::*;
use ureq_client::UreqClient;
use crate::app_menus::app_menus;
use crate::assets::Assets;
@@ -65,6 +68,13 @@ fn main() {
gpui::App::new().with_assets(Assets).run(move |cx| {
load_embedded_fonts(cx).unwrap();
let http_client = UreqClient::new(
None,
"zed_storybook".to_string(),
cx.background_executor().clone(),
);
cx.set_http_client(Arc::new(http_client));
settings::init(cx);
theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);

View File

@@ -27,7 +27,7 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
pub fn new(
mut tracker: UnboundedReceiver<String>,
notification_outlet: UnboundedSender<()>,
cx: &mut AppContext,
cx: &AppContext,
) -> Self
where
T: for<'a> Deserialize<'a> + Default + Send,
@@ -69,7 +69,7 @@ impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
mut tracker: UnboundedReceiver<String>,
notification_outlet: UnboundedSender<()>,
cx: &mut AppContext,
cx: &AppContext,
) -> Self
where
T: Default + Send,

View File

@@ -12,5 +12,6 @@ workspace = true
path = "src/telemetry_events.rs"
[dependencies]
language.workspace = true
semantic_version.workspace = true
serde.workspace = true

View File

@@ -1,5 +1,6 @@
//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information.
use language::LanguageName;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, sync::Arc, time::Duration};
@@ -153,8 +154,10 @@ pub struct AssistantEvent {
pub phase: AssistantPhase,
/// Name of the AI model used (gpt-4o, claude-3-5-sonnet, etc)
pub model: String,
pub model_provider: String,
pub response_latency: Option<Duration>,
pub error_message: Option<String>,
pub language_name: Option<LanguageName>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@@ -320,7 +320,7 @@ impl TerminalBuilder {
max_scroll_history_lines: Option<usize>,
window: AnyWindowHandle,
completion_tx: Sender<()>,
cx: &mut AppContext,
cx: &AppContext,
) -> Result<TerminalBuilder> {
// TODO: Properly set the current locale,
env.entry("LC_ALL".to_string())
@@ -455,7 +455,7 @@ impl TerminalBuilder {
})
}
pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
pub fn subscribe(mut self, cx: &ModelContext<Terminal>) -> Terminal {
//Event loop
cx.spawn(|terminal, mut cx| async move {
while let Some(event) = self.events_rx.next().await {
@@ -1280,7 +1280,7 @@ impl Terminal {
}
}
fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
//TODO: Why do these need to be doubled? Probably the same problem that the IME has
let top = region.origin.y + (self.last_content.size.line_height * 2.);
let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.);
@@ -1351,12 +1351,7 @@ impl Terminal {
}
}
pub fn mouse_up(
&mut self,
e: &MouseUpEvent,
origin: Point<Pixels>,
cx: &mut ModelContext<Self>,
) {
pub fn mouse_up(&mut self, e: &MouseUpEvent, origin: Point<Pixels>, cx: &ModelContext<Self>) {
let setting = TerminalSettings::get_global(cx);
let position = e.position - origin;
@@ -1458,9 +1453,9 @@ impl Terminal {
}
pub fn find_matches(
&mut self,
&self,
mut searcher: RegexSearch,
cx: &mut ModelContext<Self>,
cx: &ModelContext<Self>,
) -> Task<Vec<RangeInclusive<AlacPoint>>> {
let term = self.term.clone();
cx.background_executor().spawn(async move {
@@ -1530,7 +1525,7 @@ impl Terminal {
self.task.as_ref()
}
pub fn wait_for_completed_task(&self, cx: &mut AppContext) -> Task<()> {
pub fn wait_for_completed_task(&self, cx: &AppContext) -> Task<()> {
if let Some(task) = self.task() {
if task.status == TaskStatus::Running {
let mut completion_receiver = task.completion_rx.clone();

View File

@@ -189,7 +189,7 @@ impl ThemeRegistry {
}
/// Removes all themes from the registry.
pub fn clear(&mut self) {
pub fn clear(&self) {
self.state.write().themes.clear();
}

View File

@@ -498,7 +498,7 @@ pub fn observe_buffer_font_size_adjustment<V: 'static>(
}
/// Sets the adjusted buffer font size.
pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
pub fn adjusted_font_size(size: Pixels, cx: &AppContext) -> Pixels {
if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
let delta = *adjusted_size - buffer_font_size;
@@ -530,7 +530,7 @@ pub fn adjust_buffer_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
}
/// Returns whether the buffer font size has been adjusted.
pub fn has_adjusted_buffer_font_size(cx: &mut AppContext) -> bool {
pub fn has_adjusted_buffer_font_size(cx: &AppContext) -> bool {
cx.has_global::<AdjustedBufferFontSize>()
}
@@ -576,7 +576,7 @@ pub fn adjust_ui_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
}
/// Returns whether the UI font size has been adjusted.
pub fn has_adjusted_ui_font_size(cx: &mut AppContext) -> bool {
pub fn has_adjusted_ui_font_size(cx: &AppContext) -> bool {
cx.has_global::<AdjustedUiFontSize>()
}

View File

@@ -265,7 +265,7 @@ impl TitleBar {
fn render_ssh_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let host = self.project.read(cx).ssh_connection_string(cx)?;
let meta = SharedString::from(format!("Connected to: {host}"));
let indicator_color = if self.project.read(cx).ssh_is_connected()? {
let indicator_color = if self.project.read(cx).ssh_is_connected(cx)? {
Color::Success
} else {
Color::Warning

View File

@@ -2,16 +2,6 @@ use crate::prelude::*;
use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
/// The shape of an [`Avatar`].
#[derive(Debug, Default, PartialEq, Clone)]
pub enum AvatarShape {
/// The avatar is shown in a circle.
#[default]
Circle,
/// The avatar is shown in a rectangle with rounded corners.
RoundedRectangle,
}
/// An element that renders a user avatar with customizable appearance options.
///
/// # Examples
@@ -33,6 +23,7 @@ pub struct Avatar {
}
impl Avatar {
/// Creates a new avatar element with the specified image source.
pub fn new(src: impl Into<ImageSource>) -> Self {
Avatar {
image: img(src),
@@ -42,26 +33,6 @@ impl Avatar {
}
}
/// Sets the shape of the avatar image.
///
/// This method allows the shape of the avatar to be specified using an [`AvatarShape`].
/// It modifies the corner radius of the image to match the specified shape.
///
/// # Examples
///
/// ```
/// use ui::{Avatar, AvatarShape};
///
/// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
/// ```
pub fn shape(mut self, shape: AvatarShape) -> Self {
self.image = match shape {
AvatarShape::Circle => self.image.rounded_full(),
AvatarShape::RoundedRectangle => self.image.rounded_md(),
};
self
}
/// Applies a grayscale filter to the avatar image.
///
/// # Examples
@@ -76,6 +47,11 @@ impl Avatar {
self
}
/// Sets the border color of the avatar.
///
/// This might be used to match the border to the background color of
/// the parent element to create the illusion of cropping another
/// shape underneath (for example in face piles.)
pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
self.border_color = Some(color.into());
self
@@ -87,6 +63,7 @@ impl Avatar {
self
}
/// Sets the current indicator to be displayed on the avatar, if any.
pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
self.indicator = indicator.into().map(IntoElement::into_any_element);
self
@@ -94,11 +71,7 @@ impl Avatar {
}
impl RenderOnce for Avatar {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
if self.image.style().corner_radii.top_left.is_none() {
self = self.shape(AvatarShape::Circle);
}
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let border_width = if self.border_color.is_some() {
px(2.)
} else {
@@ -110,16 +83,14 @@ impl RenderOnce for Avatar {
div()
.size(container_size)
.map(|mut div| {
div.style().corner_radii = self.image.style().corner_radii.clone();
div
})
.rounded_full()
.when_some(self.border_color, |this, color| {
this.border(border_width).border_color(color)
})
.child(
self.image
.size(image_size)
.rounded_full()
.bg(cx.theme().colors().ghost_element_background),
)
.children(self.indicator.map(|indicator| div().child(indicator)))

View File

@@ -2,12 +2,17 @@ use gpui::AnyView;
use crate::prelude::*;
/// The audio status of an player, for use in representing
/// their status visually on their avatar.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum AudioStatus {
/// The player's microphone is muted.
Muted,
/// The player's microphone is muted, and collaboration audio is disabled.
Deafened,
}
/// An indicator that shows the audio status of a player.
#[derive(IntoElement)]
pub struct AvatarAudioStatusIndicator {
audio_status: AudioStatus,
@@ -15,6 +20,7 @@ pub struct AvatarAudioStatusIndicator {
}
impl AvatarAudioStatusIndicator {
/// Creates a new `AvatarAudioStatusIndicator`
pub fn new(audio_status: AudioStatus) -> Self {
Self {
audio_status,
@@ -22,6 +28,7 @@ impl AvatarAudioStatusIndicator {
}
}
/// Sets the tooltip for the indicator.
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
self.tooltip = Some(Box::new(tooltip));
self

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use crate::prelude::*;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, ElevationIndex, IconPosition, KeyBinding, Spacing};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use crate::{prelude::*, Icon, IconName, IconSize};
/// An icon that appears within a button.

View File

@@ -1,8 +1,9 @@
#![allow(missing_docs)]
use gpui::{relative, CursorStyle, DefiniteLength, MouseButton, MouseDownEvent, MouseUpEvent};
use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
use smallvec::SmallVec;
use crate::{prelude::*, Elevation, ElevationIndex, Spacing};
use crate::{prelude::*, ElevationIndex, Spacing};
/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
pub trait SelectableButton: Selectable {
@@ -145,20 +146,12 @@ pub(crate) struct ButtonLikeStyles {
pub icon_color: Hsla,
}
fn element_bg_from_elevation(elevation: Option<Elevation>, cx: &mut WindowContext) -> Hsla {
fn element_bg_from_elevation(elevation: Option<ElevationIndex>, cx: &mut WindowContext) -> Hsla {
match elevation {
Some(Elevation::ElevationIndex(ElevationIndex::Background)) => {
cx.theme().colors().element_background
}
Some(Elevation::ElevationIndex(ElevationIndex::ElevatedSurface)) => {
cx.theme().colors().surface_background
}
Some(Elevation::ElevationIndex(ElevationIndex::Surface)) => {
cx.theme().colors().elevated_surface_background
}
Some(Elevation::ElevationIndex(ElevationIndex::ModalSurface)) => {
cx.theme().colors().background
}
Some(ElevationIndex::Background) => cx.theme().colors().element_background,
Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().surface_background,
Some(ElevationIndex::Surface) => cx.theme().colors().elevated_surface_background,
Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
_ => cx.theme().colors().element_background,
}
}
@@ -166,7 +159,7 @@ fn element_bg_from_elevation(elevation: Option<Elevation>, cx: &mut WindowContex
impl ButtonStyle {
pub(crate) fn enabled(
self,
elevation: Option<Elevation>,
elevation: Option<ElevationIndex>,
cx: &mut WindowContext,
) -> ButtonLikeStyles {
let filled_background = element_bg_from_elevation(elevation, cx);
@@ -196,7 +189,7 @@ impl ButtonStyle {
pub(crate) fn hovered(
self,
elevation: Option<Elevation>,
elevation: Option<ElevationIndex>,
cx: &mut WindowContext,
) -> ButtonLikeStyles {
let mut filled_background = element_bg_from_elevation(elevation, cx);
@@ -281,7 +274,7 @@ impl ButtonStyle {
#[allow(unused)]
pub(crate) fn disabled(
self,
elevation: Option<Elevation>,
elevation: Option<ElevationIndex>,
cx: &mut WindowContext,
) -> ButtonLikeStyles {
element_bg_from_elevation(elevation, cx).fade_out(0.82);
@@ -348,7 +341,7 @@ pub struct ButtonLike {
pub(super) selected_style: Option<ButtonStyle>,
pub(super) width: Option<DefiniteLength>,
pub(super) height: Option<DefiniteLength>,
pub(super) layer: Option<Elevation>,
pub(super) layer: Option<ElevationIndex>,
size: ButtonSize,
rounding: Option<ButtonLikeRounding>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
@@ -473,7 +466,7 @@ impl ButtonCommon for ButtonLike {
}
fn layer(mut self, elevation: ElevationIndex) -> Self {
self.layer = Some(elevation.into());
self.layer = Some(elevation);
self
}
}

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{AnyView, DefiniteLength};
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{AnyView, ClickEvent};
use crate::{prelude::*, ButtonLike, ButtonLikeRounding, ElevationIndex};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
mod checkbox_with_label;
pub use checkbox_with_label::*;

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*;

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::sync::Arc;
use crate::{prelude::*, Checkbox};

View File

@@ -1,6 +1,7 @@
#![allow(missing_docs)]
use crate::{
h_flex, prelude::*, v_flex, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
ListSubHeader, WithRemSize,
h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, KeyBinding, Label, List,
ListItem, ListSeparator, ListSubHeader,
};
use gpui::{
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use std::sync::Arc;
use gpui::{ClickEvent, CursorStyle};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{Hsla, IntoElement};
use crate::prelude::*;

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{AnchorCorner, ClickEvent, CursorStyle, MouseButton, View};
use crate::{prelude::*, ContextMenu, PopoverMenu};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use crate::prelude::*;
use gpui::{AnyElement, StyleRefinement};
use smallvec::SmallVec;

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr};
@@ -285,6 +286,7 @@ pub enum IconName {
Tab,
Terminal,
Trash,
TrashAlt,
TriangleRight,
Undo,
Unpin,

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use gpui::{svg, IntoElement, Rems, RenderOnce, Size, Styled, WindowContext};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString, IntoStaticStr};

View File

@@ -1,3 +1,4 @@
#![allow(missing_docs)]
use crate::{prelude::*, AnyIcon};
#[derive(Default)]

View File

@@ -1,5 +1,7 @@
#![allow(missing_docs)]
use crate::PlatformStyle;
use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke};
use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke, WindowContext};
#[derive(IntoElement, Clone)]
pub struct KeyBinding {
@@ -192,3 +194,173 @@ impl KeyIcon {
Self { icon }
}
}
/// Returns a textual representation of the key binding for the given [`Action`].
pub fn text_for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<String> {
let key_binding = cx.bindings_for_action(action).last().cloned()?;
Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
}
/// Returns a textual representation of the key binding for the given [`Action`]
/// as if the provided [`FocusHandle`] was focused.
pub fn text_for_action_in(
action: &dyn Action,
focus: &FocusHandle,
cx: &mut WindowContext,
) -> Option<String> {
let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
Some(text_for_key_binding(key_binding, PlatformStyle::platform()))
}
/// Returns a textual representation of the given key binding for the specified platform.
pub fn text_for_key_binding(
key_binding: gpui::KeyBinding,
platform_style: PlatformStyle,
) -> String {
key_binding
.keystrokes()
.iter()
.map(|keystroke| text_for_keystroke(keystroke, platform_style))
.collect::<Vec<_>>()
.join(" ")
}
/// Returns a textual representation of the given [`Keystroke`].
pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String {
let mut text = String::new();
let delimiter = match platform_style {
PlatformStyle::Mac => '-',
PlatformStyle::Linux | PlatformStyle::Windows => '+',
};
if keystroke.modifiers.function {
match platform_style {
PlatformStyle::Mac => text.push_str("fn"),
PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"),
}
text.push(delimiter);
}
if keystroke.modifiers.control {
match platform_style {
PlatformStyle::Mac => text.push_str("Control"),
PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"),
}
text.push(delimiter);
}
if keystroke.modifiers.alt {
match platform_style {
PlatformStyle::Mac => text.push_str("Option"),
PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"),
}
text.push(delimiter);
}
if keystroke.modifiers.platform {
match platform_style {
PlatformStyle::Mac => text.push_str("Command"),
PlatformStyle::Linux => text.push_str("Super"),
PlatformStyle::Windows => text.push_str("Win"),
}
text.push(delimiter);
}
if keystroke.modifiers.shift {
match platform_style {
PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => {
text.push_str("Shift")
}
}
text.push(delimiter);
}
fn capitalize(str: &str) -> String {
let mut chars = str.chars();
match chars.next() {
None => String::new(),
Some(first_char) => first_char.to_uppercase().collect::<String>() + chars.as_str(),
}
}
let key = match keystroke.key.as_str() {
"pageup" => "PageUp",
"pagedown" => "PageDown",
key => &capitalize(key),
};
text.push_str(key);
text
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_for_keystroke() {
assert_eq!(
text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac),
"Command-C".to_string()
);
assert_eq!(
text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux),
"Super+C".to_string()
);
assert_eq!(
text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows),
"Win+C".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Mac
),
"Control-Option-Delete".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Linux
),
"Ctrl+Alt+Delete".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Windows
),
"Ctrl+Alt+Delete".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Mac
),
"Shift-PageUp".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Linux
),
"Shift+PageUp".to_string()
);
assert_eq!(
text_for_keystroke(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Windows
),
"Shift+PageUp".to_string()
);
}
}

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::ops::Range;
use gpui::{FontWeight, HighlightStyle, StyledText};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use gpui::{StyleRefinement, WindowContext};
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use gpui::{relative, AnyElement, FontWeight, StyleRefinement, Styled, UnderlineStyle};
use settings::Settings;
use smallvec::SmallVec;

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use gpui::AnyElement;
use smallvec::SmallVec;

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::sync::Arc;
use crate::{h_flex, prelude::*, Disclosure, Label};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::sync::Arc;
use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use crate::prelude::*;
#[derive(IntoElement)]

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use crate::prelude::*;
use crate::{h_flex, Icon, IconName, IconSize, Label};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use crate::{
h_flex, v_flex, Clickable, Color, Headline, HeadlineSize, IconButton, IconButtonShape,
IconName, Label, LabelCommon, LabelSize, Spacing,
@@ -260,6 +262,7 @@ impl RenderOnce for ModalFooter {
#[derive(IntoElement)]
pub struct Section {
contained: bool,
padded: bool,
header: Option<SectionHeader>,
meta: Option<SharedString>,
children: SmallVec<[AnyElement; 2]>,
@@ -275,6 +278,7 @@ impl Section {
pub fn new() -> Self {
Self {
contained: false,
padded: true,
header: None,
meta: None,
children: SmallVec::new(),
@@ -284,6 +288,7 @@ impl Section {
pub fn new_contained() -> Self {
Self {
contained: true,
padded: true,
header: None,
meta: None,
children: SmallVec::new(),
@@ -304,6 +309,10 @@ impl Section {
self.meta = Some(meta.into());
self
}
pub fn padded(mut self, padded: bool) -> Self {
self.padded = padded;
self
}
}
impl ParentElement for Section {
@@ -318,22 +327,27 @@ impl RenderOnce for Section {
section_bg.fade_out(0.96);
let children = if self.contained {
v_flex().flex_1().px(Spacing::XLarge.rems(cx)).child(
v_flex()
.w_full()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(section_bg)
.py(Spacing::Medium.rems(cx))
.gap_y(Spacing::Small.rems(cx))
.child(div().flex().flex_1().size_full().children(self.children)),
)
v_flex()
.flex_1()
.when(self.padded, |this| this.px(Spacing::XLarge.rems(cx)))
.child(
v_flex()
.w_full()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(section_bg)
.py(Spacing::Medium.rems(cx))
.gap_y(Spacing::Small.rems(cx))
.child(div().flex().flex_1().size_full().children(self.children)),
)
} else {
v_flex()
.w_full()
.gap_y(Spacing::Small.rems(cx))
.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
.when(self.padded, |this| {
this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
})
.children(self.children)
};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use gpui::ClickEvent;
use crate::{prelude::*, IconButtonShape};

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use crate::prelude::*;
use crate::v_flex;
use gpui::{

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::{cell::RefCell, rc::Rc};
use gpui::{

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