Merge main
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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.
|
||||
|
||||
7
.github/workflows/docs.yml
vendored
7
.github/workflows/docs.yml
vendored
@@ -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
15
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
assets/icons/trash_alt.svg
Normal file
1
assets/icons/trash_alt.svg
Normal 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 |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
allow-private-module-inception = true
|
||||
avoid-breaking-exported-api = false
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -910,7 +910,7 @@ impl PromptLibrary {
|
||||
.features
|
||||
.clone(),
|
||||
font_size: HeadlineSize::Large
|
||||
.size()
|
||||
.rems()
|
||||
.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]>) {
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 _
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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)>,
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
mod headless_project;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub mod unix;
|
||||
|
||||
#[cfg(test)]
|
||||
mod remote_editing_tests;
|
||||
|
||||
|
||||
336
crates/remote_server/src/unix.rs
Normal file
336
crates/remote_server/src/unix.rs
Normal 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(())
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
sqlez.workspace = true
|
||||
sqlformat = "0.2"
|
||||
sqlformat.workspace = true
|
||||
syn = "1.0"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{AnyElement, Hsla, Render};
|
||||
use story::Story;
|
||||
|
||||
use ui::{prelude::*, WithRemSize};
|
||||
use ui::{prelude::*, utils::WithRemSize};
|
||||
|
||||
pub struct WithRemSizeStory;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,5 +12,6 @@ workspace = true
|
||||
path = "src/telemetry_events.rs"
|
||||
|
||||
[dependencies]
|
||||
language.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::{prelude::*, ElevationIndex, IconPosition, KeyBinding, Spacing};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::{prelude::*, Icon, IconName, IconSize};
|
||||
|
||||
/// An icon that appears within a button.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{AnyView, ClickEvent};
|
||||
|
||||
use crate::{prelude::*, ButtonLike, ButtonLikeRounding, ElevationIndex};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
mod checkbox_with_label;
|
||||
|
||||
pub use checkbox_with_label::*;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{prelude::*, Checkbox};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{ClickEvent, CursorStyle};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{Hsla, IntoElement};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{AnchorCorner, ClickEvent, CursorStyle, MouseButton, View};
|
||||
|
||||
use crate::{prelude::*, ContextMenu, PopoverMenu};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::prelude::*;
|
||||
use gpui::{AnyElement, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::{prelude::*, AnyIcon};
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{FontWeight, HighlightStyle, StyledText};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::{StyleRefinement, WindowContext};
|
||||
|
||||
use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::{relative, AnyElement, FontWeight, StyleRefinement, Styled, UnderlineStyle};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::AnyElement;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{h_flex, prelude::*, Disclosure, Label};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_flex, Icon, IconName, IconSize, Label};
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::ClickEvent;
|
||||
|
||||
use crate::{prelude::*, IconButtonShape};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::v_flex;
|
||||
use gpui::{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user