Remove CRLF line endings (#57680)

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A or Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2026-05-26 08:59:21 -04:00 committed by GitHub
parent 63ff99795c
commit 1dac14c53a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 3365 additions and 3365 deletions

View file

@ -1,333 +1,333 @@
use gpui::{
PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant,
ThreadTaskTimings,
};
use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use web_time::Instant;
#[cfg(feature = "multithreaded")]
const MIN_BACKGROUND_THREADS: usize = 2;
#[cfg(feature = "multithreaded")]
fn shared_memory_supported() -> bool {
let global = js_sys::global();
let has_shared_array_buffer =
js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false);
let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false);
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
let buffer = memory.buffer();
let is_shared_buffer = buffer.is_instance_of::<js_sys::SharedArrayBuffer>();
has_shared_array_buffer && has_atomics && is_shared_buffer
}
enum MainThreadItem {
Runnable(RunnableVariant),
Delayed {
runnable: RunnableVariant,
millis: i32,
},
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
RealtimeFunction(Box<dyn FnOnce() + Send>),
}
struct MainThreadMailbox {
sender: PriorityQueueSender<MainThreadItem>,
receiver: parking_lot::Mutex<PriorityQueueReceiver<MainThreadItem>>,
signal: AtomicI32,
}
impl MainThreadMailbox {
fn new() -> Self {
let (sender, receiver) = PriorityQueueReceiver::new();
Self {
sender,
receiver: parking_lot::Mutex::new(receiver),
signal: AtomicI32::new(0),
}
}
fn post(&self, priority: Priority, item: MainThreadItem) {
if self.sender.spin_send(priority, item).is_err() {
log::error!("MainThreadMailbox::send failed: receiver disconnected");
}
// TODO-Wasm: Verify this lock-free protocol
let view = self.signal_view();
js_sys::Atomics::store(&view, 0, 1).ok();
js_sys::Atomics::notify(&view, 0).ok();
}
fn drain(&self, window: &web_sys::Window) {
let mut receiver = self.receiver.lock();
loop {
// We need these `spin` variants because we can't acquire a lock on the main thread.
// TODO-WASM: Should we do something different?
match receiver.spin_try_pop() {
Ok(Some(item)) => execute_on_main_thread(window, item),
Ok(None) => break,
Err(_) => break,
}
}
}
fn signal_view(&self) -> js_sys::Int32Array {
let byte_offset = self.signal.as_ptr() as u32;
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1)
}
fn run_waker_loop(self: &Arc<Self>, window: web_sys::Window) {
if !shared_memory_supported() {
log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled");
return;
}
let mailbox = Arc::clone(self);
wasm_bindgen_futures::spawn_local(async move {
let view = mailbox.signal_view();
loop {
js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed");
let result = match js_sys::Atomics::wait_async(&view, 0, 0) {
Ok(result) => result,
Err(error) => {
log::error!("Atomics.waitAsync failed: {error:?}");
break;
}
};
let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !is_async {
log::error!("Atomics.waitAsync returned synchronously; waker loop exiting");
break;
}
let promise: js_sys::Promise =
js_sys::Reflect::get(&result, &JsValue::from_str("value"))
.expect("waitAsync result missing 'value'")
.unchecked_into();
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
mailbox.drain(&window);
}
});
}
}
pub struct WebDispatcher {
main_thread_id: std::thread::ThreadId,
browser_window: web_sys::Window,
background_sender: PriorityQueueSender<RunnableVariant>,
main_thread_mailbox: Arc<MainThreadMailbox>,
supports_threads: bool,
#[cfg(feature = "multithreaded")]
_background_threads: Vec<wasm_thread::JoinHandle<()>>,
}
// Safety: `web_sys::Window` is only accessed from the main thread
// All other fields are `Send + Sync` by construction.
unsafe impl Send for WebDispatcher {}
unsafe impl Sync for WebDispatcher {}
impl WebDispatcher {
pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self {
#[cfg(feature = "multithreaded")]
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
#[cfg(not(feature = "multithreaded"))]
let (background_sender, _) = PriorityQueueReceiver::new();
let main_thread_mailbox = Arc::new(MainThreadMailbox::new());
#[cfg(feature = "multithreaded")]
let supports_threads = allow_threads && shared_memory_supported();
#[cfg(not(feature = "multithreaded"))]
let supports_threads = false;
if supports_threads {
main_thread_mailbox.run_waker_loop(browser_window.clone());
} else {
log::warn!(
"SharedArrayBuffer not available; falling back to single-threaded dispatcher"
);
}
#[cfg(feature = "multithreaded")]
let background_threads = if supports_threads {
let thread_count = browser_window
.navigator()
.hardware_concurrency()
.max(MIN_BACKGROUND_THREADS as f64) as usize;
// TODO-Wasm: Is it bad to have web workers blocking for a long time like this?
(0..thread_count)
.map(|i| {
let mut receiver = background_receiver.clone();
wasm_thread::Builder::new()
.name(format!("background-worker-{i}"))
.spawn(move || {
loop {
let runnable: RunnableVariant = match receiver.pop() {
Ok(runnable) => runnable,
Err(_) => {
log::info!(
"background-worker-{i}: channel disconnected, exiting"
);
break;
}
};
runnable.run();
}
})
.expect("failed to spawn background worker thread")
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
Self {
main_thread_id: std::thread::current().id(),
browser_window,
background_sender,
main_thread_mailbox,
supports_threads,
#[cfg(feature = "multithreaded")]
_background_threads: background_threads,
}
}
fn on_main_thread(&self) -> bool {
std::thread::current().id() == self.main_thread_id
}
}
impl PlatformDispatcher for WebDispatcher {
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
// TODO-Wasm: should we panic here?
Vec::new()
}
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
ThreadTaskTimings {
thread_name: None,
thread_id: std::thread::current().id(),
timings: Vec::new(),
total_pushed: 0,
}
}
fn is_main_thread(&self) -> bool {
self.on_main_thread()
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
if !self.supports_threads {
self.dispatch_on_main_thread(runnable, priority);
return;
}
let result = if self.on_main_thread() {
self.background_sender.spin_send(priority, runnable)
} else {
self.background_sender.send(priority, runnable)
};
if let Err(error) = result {
log::error!("dispatch: failed to send to background queue: {error:?}");
}
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
if self.on_main_thread() {
schedule_runnable(&self.browser_window, runnable, priority);
} else {
self.main_thread_mailbox
.post(priority, MainThreadItem::Runnable(runnable));
}
}
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
let millis = duration.as_millis().min(i32::MAX as u128) as i32;
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
runnable.run();
});
self.browser_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::Delayed { runnable, millis });
}
}
fn spawn_realtime(&self, function: Box<dyn FnOnce() + Send>) {
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
function();
});
self.browser_window
.queue_microtask(callback.unchecked_ref());
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::RealtimeFunction(function));
}
}
fn now(&self) -> Instant {
Instant::now()
}
}
fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
match item {
MainThreadItem::Runnable(runnable) => {
runnable.run();
}
MainThreadItem::Delayed { runnable, millis } => {
let callback = Closure::once_into_js(move || {
runnable.run();
});
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
}
MainThreadItem::RealtimeFunction(function) => {
function();
}
}
}
fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) {
let callback = Closure::once_into_js(move || {
runnable.run();
});
let callback: &js_sys::Function = callback.unchecked_ref();
match priority {
Priority::RealtimeAudio => {
window.queue_microtask(callback);
}
_ => {
// TODO-Wasm: this ought to enqueue so we can dequeue with proper priority
window
.set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0)
.ok();
}
}
}
use gpui::{
PlatformDispatcher, Priority, PriorityQueueReceiver, PriorityQueueSender, RunnableVariant,
ThreadTaskTimings,
};
use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use web_time::Instant;
#[cfg(feature = "multithreaded")]
const MIN_BACKGROUND_THREADS: usize = 2;
#[cfg(feature = "multithreaded")]
fn shared_memory_supported() -> bool {
let global = js_sys::global();
let has_shared_array_buffer =
js_sys::Reflect::has(&global, &JsValue::from_str("SharedArrayBuffer")).unwrap_or(false);
let has_atomics = js_sys::Reflect::has(&global, &JsValue::from_str("Atomics")).unwrap_or(false);
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
let buffer = memory.buffer();
let is_shared_buffer = buffer.is_instance_of::<js_sys::SharedArrayBuffer>();
has_shared_array_buffer && has_atomics && is_shared_buffer
}
enum MainThreadItem {
Runnable(RunnableVariant),
Delayed {
runnable: RunnableVariant,
millis: i32,
},
// TODO-Wasm: Shouldn't these run on their own dedicated thread?
RealtimeFunction(Box<dyn FnOnce() + Send>),
}
struct MainThreadMailbox {
sender: PriorityQueueSender<MainThreadItem>,
receiver: parking_lot::Mutex<PriorityQueueReceiver<MainThreadItem>>,
signal: AtomicI32,
}
impl MainThreadMailbox {
fn new() -> Self {
let (sender, receiver) = PriorityQueueReceiver::new();
Self {
sender,
receiver: parking_lot::Mutex::new(receiver),
signal: AtomicI32::new(0),
}
}
fn post(&self, priority: Priority, item: MainThreadItem) {
if self.sender.spin_send(priority, item).is_err() {
log::error!("MainThreadMailbox::send failed: receiver disconnected");
}
// TODO-Wasm: Verify this lock-free protocol
let view = self.signal_view();
js_sys::Atomics::store(&view, 0, 1).ok();
js_sys::Atomics::notify(&view, 0).ok();
}
fn drain(&self, window: &web_sys::Window) {
let mut receiver = self.receiver.lock();
loop {
// We need these `spin` variants because we can't acquire a lock on the main thread.
// TODO-WASM: Should we do something different?
match receiver.spin_try_pop() {
Ok(Some(item)) => execute_on_main_thread(window, item),
Ok(None) => break,
Err(_) => break,
}
}
}
fn signal_view(&self) -> js_sys::Int32Array {
let byte_offset = self.signal.as_ptr() as u32;
let memory = js_sys::WebAssembly::Memory::from(wasm_bindgen::memory());
js_sys::Int32Array::new_with_byte_offset_and_length(&memory.buffer(), byte_offset, 1)
}
fn run_waker_loop(self: &Arc<Self>, window: web_sys::Window) {
if !shared_memory_supported() {
log::warn!("SharedArrayBuffer not available; main thread mailbox waker loop disabled");
return;
}
let mailbox = Arc::clone(self);
wasm_bindgen_futures::spawn_local(async move {
let view = mailbox.signal_view();
loop {
js_sys::Atomics::store(&view, 0, 0).expect("Atomics.store failed");
let result = match js_sys::Atomics::wait_async(&view, 0, 0) {
Ok(result) => result,
Err(error) => {
log::error!("Atomics.waitAsync failed: {error:?}");
break;
}
};
let is_async = js_sys::Reflect::get(&result, &JsValue::from_str("async"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !is_async {
log::error!("Atomics.waitAsync returned synchronously; waker loop exiting");
break;
}
let promise: js_sys::Promise =
js_sys::Reflect::get(&result, &JsValue::from_str("value"))
.expect("waitAsync result missing 'value'")
.unchecked_into();
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
mailbox.drain(&window);
}
});
}
}
pub struct WebDispatcher {
main_thread_id: std::thread::ThreadId,
browser_window: web_sys::Window,
background_sender: PriorityQueueSender<RunnableVariant>,
main_thread_mailbox: Arc<MainThreadMailbox>,
supports_threads: bool,
#[cfg(feature = "multithreaded")]
_background_threads: Vec<wasm_thread::JoinHandle<()>>,
}
// Safety: `web_sys::Window` is only accessed from the main thread
// All other fields are `Send + Sync` by construction.
unsafe impl Send for WebDispatcher {}
unsafe impl Sync for WebDispatcher {}
impl WebDispatcher {
pub fn new(browser_window: web_sys::Window, allow_threads: bool) -> Self {
#[cfg(feature = "multithreaded")]
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
#[cfg(not(feature = "multithreaded"))]
let (background_sender, _) = PriorityQueueReceiver::new();
let main_thread_mailbox = Arc::new(MainThreadMailbox::new());
#[cfg(feature = "multithreaded")]
let supports_threads = allow_threads && shared_memory_supported();
#[cfg(not(feature = "multithreaded"))]
let supports_threads = false;
if supports_threads {
main_thread_mailbox.run_waker_loop(browser_window.clone());
} else {
log::warn!(
"SharedArrayBuffer not available; falling back to single-threaded dispatcher"
);
}
#[cfg(feature = "multithreaded")]
let background_threads = if supports_threads {
let thread_count = browser_window
.navigator()
.hardware_concurrency()
.max(MIN_BACKGROUND_THREADS as f64) as usize;
// TODO-Wasm: Is it bad to have web workers blocking for a long time like this?
(0..thread_count)
.map(|i| {
let mut receiver = background_receiver.clone();
wasm_thread::Builder::new()
.name(format!("background-worker-{i}"))
.spawn(move || {
loop {
let runnable: RunnableVariant = match receiver.pop() {
Ok(runnable) => runnable,
Err(_) => {
log::info!(
"background-worker-{i}: channel disconnected, exiting"
);
break;
}
};
runnable.run();
}
})
.expect("failed to spawn background worker thread")
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
Self {
main_thread_id: std::thread::current().id(),
browser_window,
background_sender,
main_thread_mailbox,
supports_threads,
#[cfg(feature = "multithreaded")]
_background_threads: background_threads,
}
}
fn on_main_thread(&self) -> bool {
std::thread::current().id() == self.main_thread_id
}
}
impl PlatformDispatcher for WebDispatcher {
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
// TODO-Wasm: should we panic here?
Vec::new()
}
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
ThreadTaskTimings {
thread_name: None,
thread_id: std::thread::current().id(),
timings: Vec::new(),
total_pushed: 0,
}
}
fn is_main_thread(&self) -> bool {
self.on_main_thread()
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
if !self.supports_threads {
self.dispatch_on_main_thread(runnable, priority);
return;
}
let result = if self.on_main_thread() {
self.background_sender.spin_send(priority, runnable)
} else {
self.background_sender.send(priority, runnable)
};
if let Err(error) = result {
log::error!("dispatch: failed to send to background queue: {error:?}");
}
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
if self.on_main_thread() {
schedule_runnable(&self.browser_window, runnable, priority);
} else {
self.main_thread_mailbox
.post(priority, MainThreadItem::Runnable(runnable));
}
}
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
let millis = duration.as_millis().min(i32::MAX as u128) as i32;
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
runnable.run();
});
self.browser_window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::Delayed { runnable, millis });
}
}
fn spawn_realtime(&self, function: Box<dyn FnOnce() + Send>) {
if self.on_main_thread() {
let callback = Closure::once_into_js(move || {
function();
});
self.browser_window
.queue_microtask(callback.unchecked_ref());
} else {
self.main_thread_mailbox
.post(Priority::High, MainThreadItem::RealtimeFunction(function));
}
}
fn now(&self) -> Instant {
Instant::now()
}
}
fn execute_on_main_thread(window: &web_sys::Window, item: MainThreadItem) {
match item {
MainThreadItem::Runnable(runnable) => {
runnable.run();
}
MainThreadItem::Delayed { runnable, millis } => {
let callback = Closure::once_into_js(move || {
runnable.run();
});
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok();
}
MainThreadItem::RealtimeFunction(function) => {
function();
}
}
}
fn schedule_runnable(window: &web_sys::Window, runnable: RunnableVariant, priority: Priority) {
let callback = Closure::once_into_js(move || {
runnable.run();
});
let callback: &js_sys::Function = callback.unchecked_ref();
match priority {
Priority::RealtimeAudio => {
window.queue_microtask(callback);
}
_ => {
// TODO-Wasm: this ought to enqueue so we can dequeue with proper priority
window
.set_timeout_with_callback_and_timeout_and_arguments_0(callback, 0)
.ok();
}
}
}

View file

@ -1,98 +1,98 @@
use anyhow::Result;
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px};
#[derive(Debug)]
pub struct WebDisplay {
id: DisplayId,
uuid: uuid::Uuid,
browser_window: web_sys::Window,
}
// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`.
unsafe impl Send for WebDisplay {}
unsafe impl Sync for WebDisplay {}
impl WebDisplay {
pub fn new(browser_window: web_sys::Window) -> Self {
WebDisplay {
id: DisplayId::new(1),
uuid: uuid::Uuid::new_v4(),
browser_window,
}
}
fn screen_size(&self) -> Size<Pixels> {
let Some(screen) = self.browser_window.screen().ok() else {
return Size {
width: px(1920.),
height: px(1080.),
};
};
let width = screen.width().unwrap_or(1920) as f32;
let height = screen.height().unwrap_or(1080) as f32;
Size {
width: px(width),
height: px(height),
}
}
fn viewport_size(&self) -> Size<Pixels> {
let width = self
.browser_window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1920.0) as f32;
let height = self
.browser_window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1080.0) as f32;
Size {
width: px(width),
height: px(height),
}
}
}
impl PlatformDisplay for WebDisplay {
fn id(&self) -> DisplayId {
self.id
}
fn uuid(&self) -> Result<uuid::Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
let size = self.screen_size();
Bounds {
origin: Point::default(),
size,
}
}
fn visible_bounds(&self) -> Bounds<Pixels> {
let size = self.viewport_size();
Bounds {
origin: Point::default(),
size,
}
}
fn default_bounds(&self) -> Bounds<Pixels> {
let visible = self.visible_bounds();
let width = visible.size.width * 0.75;
let height = visible.size.height * 0.75;
let origin_x = (visible.size.width - width) / 2.0;
let origin_y = (visible.size.height - height) / 2.0;
Bounds {
origin: Point::new(origin_x, origin_y),
size: Size { width, height },
}
}
}
use anyhow::Result;
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Point, Size, px};
#[derive(Debug)]
pub struct WebDisplay {
id: DisplayId,
uuid: uuid::Uuid,
browser_window: web_sys::Window,
}
// Safety: WASM is single-threaded — there is no concurrent access to `web_sys::Window`.
unsafe impl Send for WebDisplay {}
unsafe impl Sync for WebDisplay {}
impl WebDisplay {
pub fn new(browser_window: web_sys::Window) -> Self {
WebDisplay {
id: DisplayId::new(1),
uuid: uuid::Uuid::new_v4(),
browser_window,
}
}
fn screen_size(&self) -> Size<Pixels> {
let Some(screen) = self.browser_window.screen().ok() else {
return Size {
width: px(1920.),
height: px(1080.),
};
};
let width = screen.width().unwrap_or(1920) as f32;
let height = screen.height().unwrap_or(1080) as f32;
Size {
width: px(width),
height: px(height),
}
}
fn viewport_size(&self) -> Size<Pixels> {
let width = self
.browser_window
.inner_width()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1920.0) as f32;
let height = self
.browser_window
.inner_height()
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(1080.0) as f32;
Size {
width: px(width),
height: px(height),
}
}
}
impl PlatformDisplay for WebDisplay {
fn id(&self) -> DisplayId {
self.id
}
fn uuid(&self) -> Result<uuid::Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
let size = self.screen_size();
Bounds {
origin: Point::default(),
size,
}
}
fn visible_bounds(&self) -> Bounds<Pixels> {
let size = self.viewport_size();
Bounds {
origin: Point::default(),
size,
}
}
fn default_bounds(&self) -> Bounds<Pixels> {
let visible = self.visible_bounds();
let width = visible.size.width * 0.75;
let height = visible.size.height * 0.75;
let origin_x = (visible.size.width - width) / 2.0;
let origin_y = (visible.size.height - height) / 2.0;
Bounds {
origin: Point::new(origin_x, origin_y),
size: Size { width, height },
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,19 @@
use gpui::PlatformKeyboardLayout;
pub struct WebKeyboardLayout;
impl WebKeyboardLayout {
pub fn new() -> Self {
WebKeyboardLayout
}
}
impl PlatformKeyboardLayout for WebKeyboardLayout {
fn id(&self) -> &str {
"us"
}
fn name(&self) -> &str {
"US"
}
}
use gpui::PlatformKeyboardLayout;
pub struct WebKeyboardLayout;
impl WebKeyboardLayout {
pub fn new() -> Self {
WebKeyboardLayout
}
}
impl PlatformKeyboardLayout for WebKeyboardLayout {
fn id(&self) -> &str {
"us"
}
fn name(&self) -> &str {
"US"
}
}

View file

@ -1,37 +1,37 @@
use log::{Level, Log, Metadata, Record};
struct ConsoleLogger;
impl Log for ConsoleLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let message = format!(
"[{}] {}: {}",
record.level(),
record.target(),
record.args()
);
let js_string = wasm_bindgen::JsValue::from_str(&message);
match record.level() {
Level::Error => web_sys::console::error_1(&js_string),
Level::Warn => web_sys::console::warn_1(&js_string),
Level::Info => web_sys::console::info_1(&js_string),
Level::Debug | Level::Trace => web_sys::console::log_1(&js_string),
}
}
fn flush(&self) {}
}
pub fn init_logging() {
log::set_logger(&ConsoleLogger).ok();
log::set_max_level(log::LevelFilter::Info);
}
use log::{Level, Log, Metadata, Record};
struct ConsoleLogger;
impl Log for ConsoleLogger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let message = format!(
"[{}] {}: {}",
record.level(),
record.target(),
record.args()
);
let js_string = wasm_bindgen::JsValue::from_str(&message);
match record.level() {
Level::Error => web_sys::console::error_1(&js_string),
Level::Warn => web_sys::console::warn_1(&js_string),
Level::Info => web_sys::console::info_1(&js_string),
Level::Debug | Level::Trace => web_sys::console::log_1(&js_string),
}
}
fn flush(&self) {}
}
pub fn init_logging() {
log::set_logger(&ConsoleLogger).ok();
log::set_max_level(log::LevelFilter::Info);
}

View file

@ -1,435 +1,435 @@
use crate::dispatcher::WebDispatcher;
use crate::display::WebDisplay;
use crate::keyboard::WebKeyboardLayout;
use crate::window::WebWindow;
use anyhow::Result;
use futures::channel::oneshot;
use gpui::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
ThermalState, WindowAppearance, WindowParams,
};
use gpui_wgpu::WgpuContext;
use std::{
borrow::Cow,
cell::{Cell, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use wasm_bindgen::prelude::*;
static BUNDLED_FONTS: &[&[u8]] = &[
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
];
pub struct WebPlatform {
browser_window: web_sys::Window,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
active_window: RefCell<Option<AnyWindowHandle>>,
active_display: Rc<dyn PlatformDisplay>,
callbacks: RefCell<WebPlatformCallbacks>,
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
_cursor_restore_listeners: Vec<EventListenerHandle>,
}
#[derive(Default)]
struct WebPlatformCallbacks {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
quit: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>,
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
will_open_app_menu: Option<Box<dyn FnMut()>>,
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
keyboard_layout_change: Option<Box<dyn FnMut()>>,
thermal_state_change: Option<Box<dyn FnMut()>>,
}
impl WebPlatform {
pub fn new(allow_multi_threading: bool) -> Self {
let browser_window =
web_sys::window().expect("must be running in a browser window context");
let dispatcher = Arc::new(WebDispatcher::new(
browser_window.clone(),
allow_multi_threading,
));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
"IBM Plex Sans",
));
let fonts = BUNDLED_FONTS
.iter()
.map(|bytes| Cow::Borrowed(*bytes))
.collect();
if let Err(error) = text_system.add_fonts(fonts) {
log::error!("failed to load bundled fonts: {error:#}");
}
let text_system: Arc<dyn PlatformTextSystem> = text_system;
let active_display: Rc<dyn PlatformDisplay> =
Rc::new(WebDisplay::new(browser_window.clone()));
let cursor_visible = Rc::new(Cell::new(true));
let last_cursor_css = Rc::new(Cell::new("default"));
let cursor_restore_listeners = cursor_restore_listeners(
&browser_window,
cursor_visible.clone(),
last_cursor_css.clone(),
);
Self {
browser_window,
background_executor,
foreground_executor,
text_system,
active_window: RefCell::new(None),
active_display,
callbacks: RefCell::new(WebPlatformCallbacks::default()),
wgpu_context: Rc::new(RefCell::new(None)),
cursor_visible,
last_cursor_css,
_cursor_restore_listeners: cursor_restore_listeners,
}
}
}
impl Platform for WebPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
let wgpu_context = self.wgpu_context.clone();
wasm_bindgen_futures::spawn_local(async move {
match WgpuContext::new_web().await {
Ok(context) => {
log::info!("WebGPU context initialized successfully");
*wgpu_context.borrow_mut() = Some(context);
on_finish_launching();
}
Err(err) => {
log::error!("Failed to initialize WebGPU context: {err:#}");
on_finish_launching();
}
}
});
}
fn quit(&self) {
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
}
fn restart(&self, _binary_path: Option<PathBuf>) {}
fn activate(&self, _ignoring_other_apps: bool) {}
fn hide(&self) {}
fn hide_other_apps(&self) {}
fn unhide_other_apps(&self) {}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![self.active_display.clone()]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.active_display.clone())
}
fn active_window(&self) -> Option<AnyWindowHandle> {
*self.active_window.borrow()
}
fn open_window(
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let context_ref = self.wgpu_context.borrow();
let context = context_ref.as_ref().ok_or_else(|| {
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
})?;
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
*self.active_window.borrow_mut() = Some(handle);
Ok(Box::new(window))
}
fn window_appearance(&self) -> WindowAppearance {
let Ok(Some(media_query)) = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
else {
return WindowAppearance::Light;
};
if media_query.matches() {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
fn open_url(&self, url: &str) {
if let Err(error) = self.browser_window.open_with_url(url) {
log::warn!("Failed to open URL '{url}': {error:?}");
}
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.callbacks.borrow_mut().open_urls = Some(callback);
}
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn prompt_for_paths(
&self,
_options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
let (tx, rx) = oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"prompt_for_paths is not supported on the web"
)))
.ok();
rx
}
fn prompt_for_new_path(
&self,
_directory: &Path,
_suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (sender, receiver) = oneshot::channel();
sender
.send(Err(anyhow::anyhow!(
"prompt_for_new_path is not supported on the web"
)))
.ok();
receiver
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
fn reveal_path(&self, _path: &Path) {}
fn open_with_system(&self, _path: &Path) {}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().reopen = Some(callback);
}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.callbacks.borrow_mut().app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
}
fn thermal_state(&self) -> ThermalState {
ThermalState::Nominal
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
}
fn compositor_name(&self) -> &'static str {
"Web"
}
fn app_path(&self) -> Result<PathBuf> {
Err(anyhow::anyhow!("app_path is not available on the web"))
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow::anyhow!(
"path_for_auxiliary_executable is not available on the web"
))
}
fn set_cursor_style(&self, style: CursorStyle) {
let css_cursor = match style {
CursorStyle::Arrow => "default",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",
CursorStyle::OpenHand => "grab",
CursorStyle::PointingHand => "pointer",
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
"ew-resize"
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
"ns-resize"
}
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
};
self.last_cursor_css.set(css_cursor);
if self.cursor_visible.get() {
set_body_cursor(&self.browser_window, css_cursor);
}
}
fn hide_cursor_until_mouse_moves(&self) {
if !self.cursor_visible.replace(false) {
return;
}
set_body_cursor(&self.browser_window, "none");
}
fn is_cursor_visible(&self) -> bool {
self.cursor_visible.get()
}
fn should_auto_hide_scrollbars(&self) -> bool {
true
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
None
}
fn write_to_clipboard(&self, _item: ClipboardItem) {}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
Task::ready(Ok(None))
}
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(WebKeyboardLayout)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
}
}
struct EventListenerHandle {
target: web_sys::EventTarget,
event_name: &'static str,
closure: Closure<dyn FnMut(JsValue)>,
}
impl Drop for EventListenerHandle {
fn drop(&mut self) {
self.target
.remove_event_listener_with_callback(
self.event_name,
self.closure.as_ref().unchecked_ref(),
)
.ok();
}
}
fn cursor_restore_listeners(
browser_window: &web_sys::Window,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
) -> Vec<EventListenerHandle> {
let mut handles = Vec::new();
let Some(document) = browser_window.document() else {
return handles;
};
let make_restore_handler = |browser_window: web_sys::Window| {
let cursor_visible = cursor_visible.clone();
let last_cursor_css = last_cursor_css.clone();
Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
if !cursor_visible.replace(true) {
set_body_cursor(&browser_window, last_cursor_css.get());
}
})
};
let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {
let closure = make_restore_handler(browser_window.clone());
target
.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
.ok();
handles.push(EventListenerHandle {
target: target.clone(),
event_name,
closure,
});
};
let document_target: &web_sys::EventTarget = document.as_ref();
let window_target: &web_sys::EventTarget = browser_window.as_ref();
add_listener(document_target, "mousemove");
add_listener(document_target, "mouseenter");
add_listener(window_target, "blur");
add_listener(document_target, "visibilitychange");
handles
}
fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) {
if let Some(document) = browser_window.document()
&& let Some(body) = document.body()
&& let Err(error) = body.style().set_property("cursor", css_cursor)
{
log::warn!("Failed to set cursor style: {error:?}");
}
}
use crate::dispatcher::WebDispatcher;
use crate::display::WebDisplay;
use crate::keyboard::WebKeyboardLayout;
use crate::window::WebWindow;
use anyhow::Result;
use futures::channel::oneshot;
use gpui::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
ThermalState, WindowAppearance, WindowParams,
};
use gpui_wgpu::WgpuContext;
use std::{
borrow::Cow,
cell::{Cell, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use wasm_bindgen::prelude::*;
static BUNDLED_FONTS: &[&[u8]] = &[
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
];
pub struct WebPlatform {
browser_window: web_sys::Window,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
active_window: RefCell<Option<AnyWindowHandle>>,
active_display: Rc<dyn PlatformDisplay>,
callbacks: RefCell<WebPlatformCallbacks>,
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
_cursor_restore_listeners: Vec<EventListenerHandle>,
}
#[derive(Default)]
struct WebPlatformCallbacks {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
quit: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>,
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
will_open_app_menu: Option<Box<dyn FnMut()>>,
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
keyboard_layout_change: Option<Box<dyn FnMut()>>,
thermal_state_change: Option<Box<dyn FnMut()>>,
}
impl WebPlatform {
pub fn new(allow_multi_threading: bool) -> Self {
let browser_window =
web_sys::window().expect("must be running in a browser window context");
let dispatcher = Arc::new(WebDispatcher::new(
browser_window.clone(),
allow_multi_threading,
));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
"IBM Plex Sans",
));
let fonts = BUNDLED_FONTS
.iter()
.map(|bytes| Cow::Borrowed(*bytes))
.collect();
if let Err(error) = text_system.add_fonts(fonts) {
log::error!("failed to load bundled fonts: {error:#}");
}
let text_system: Arc<dyn PlatformTextSystem> = text_system;
let active_display: Rc<dyn PlatformDisplay> =
Rc::new(WebDisplay::new(browser_window.clone()));
let cursor_visible = Rc::new(Cell::new(true));
let last_cursor_css = Rc::new(Cell::new("default"));
let cursor_restore_listeners = cursor_restore_listeners(
&browser_window,
cursor_visible.clone(),
last_cursor_css.clone(),
);
Self {
browser_window,
background_executor,
foreground_executor,
text_system,
active_window: RefCell::new(None),
active_display,
callbacks: RefCell::new(WebPlatformCallbacks::default()),
wgpu_context: Rc::new(RefCell::new(None)),
cursor_visible,
last_cursor_css,
_cursor_restore_listeners: cursor_restore_listeners,
}
}
}
impl Platform for WebPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
let wgpu_context = self.wgpu_context.clone();
wasm_bindgen_futures::spawn_local(async move {
match WgpuContext::new_web().await {
Ok(context) => {
log::info!("WebGPU context initialized successfully");
*wgpu_context.borrow_mut() = Some(context);
on_finish_launching();
}
Err(err) => {
log::error!("Failed to initialize WebGPU context: {err:#}");
on_finish_launching();
}
}
});
}
fn quit(&self) {
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
}
fn restart(&self, _binary_path: Option<PathBuf>) {}
fn activate(&self, _ignoring_other_apps: bool) {}
fn hide(&self) {}
fn hide_other_apps(&self) {}
fn unhide_other_apps(&self) {}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![self.active_display.clone()]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.active_display.clone())
}
fn active_window(&self) -> Option<AnyWindowHandle> {
*self.active_window.borrow()
}
fn open_window(
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let context_ref = self.wgpu_context.borrow();
let context = context_ref.as_ref().ok_or_else(|| {
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
})?;
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
*self.active_window.borrow_mut() = Some(handle);
Ok(Box::new(window))
}
fn window_appearance(&self) -> WindowAppearance {
let Ok(Some(media_query)) = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
else {
return WindowAppearance::Light;
};
if media_query.matches() {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
fn open_url(&self, url: &str) {
if let Err(error) = self.browser_window.open_with_url(url) {
log::warn!("Failed to open URL '{url}': {error:?}");
}
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.callbacks.borrow_mut().open_urls = Some(callback);
}
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn prompt_for_paths(
&self,
_options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
let (tx, rx) = oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"prompt_for_paths is not supported on the web"
)))
.ok();
rx
}
fn prompt_for_new_path(
&self,
_directory: &Path,
_suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (sender, receiver) = oneshot::channel();
sender
.send(Err(anyhow::anyhow!(
"prompt_for_new_path is not supported on the web"
)))
.ok();
receiver
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
fn reveal_path(&self, _path: &Path) {}
fn open_with_system(&self, _path: &Path) {}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().reopen = Some(callback);
}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.callbacks.borrow_mut().app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
}
fn thermal_state(&self) -> ThermalState {
ThermalState::Nominal
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
}
fn compositor_name(&self) -> &'static str {
"Web"
}
fn app_path(&self) -> Result<PathBuf> {
Err(anyhow::anyhow!("app_path is not available on the web"))
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow::anyhow!(
"path_for_auxiliary_executable is not available on the web"
))
}
fn set_cursor_style(&self, style: CursorStyle) {
let css_cursor = match style {
CursorStyle::Arrow => "default",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",
CursorStyle::OpenHand => "grab",
CursorStyle::PointingHand => "pointer",
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
"ew-resize"
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
"ns-resize"
}
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
};
self.last_cursor_css.set(css_cursor);
if self.cursor_visible.get() {
set_body_cursor(&self.browser_window, css_cursor);
}
}
fn hide_cursor_until_mouse_moves(&self) {
if !self.cursor_visible.replace(false) {
return;
}
set_body_cursor(&self.browser_window, "none");
}
fn is_cursor_visible(&self) -> bool {
self.cursor_visible.get()
}
fn should_auto_hide_scrollbars(&self) -> bool {
true
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
None
}
fn write_to_clipboard(&self, _item: ClipboardItem) {}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
Task::ready(Ok(None))
}
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(WebKeyboardLayout)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
}
}
struct EventListenerHandle {
target: web_sys::EventTarget,
event_name: &'static str,
closure: Closure<dyn FnMut(JsValue)>,
}
impl Drop for EventListenerHandle {
fn drop(&mut self) {
self.target
.remove_event_listener_with_callback(
self.event_name,
self.closure.as_ref().unchecked_ref(),
)
.ok();
}
}
fn cursor_restore_listeners(
browser_window: &web_sys::Window,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
) -> Vec<EventListenerHandle> {
let mut handles = Vec::new();
let Some(document) = browser_window.document() else {
return handles;
};
let make_restore_handler = |browser_window: web_sys::Window| {
let cursor_visible = cursor_visible.clone();
let last_cursor_css = last_cursor_css.clone();
Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
if !cursor_visible.replace(true) {
set_body_cursor(&browser_window, last_cursor_css.get());
}
})
};
let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {
let closure = make_restore_handler(browser_window.clone());
target
.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
.ok();
handles.push(EventListenerHandle {
target: target.clone(),
event_name,
closure,
});
};
let document_target: &web_sys::EventTarget = document.as_ref();
let window_target: &web_sys::EventTarget = browser_window.as_ref();
add_listener(document_target, "mousemove");
add_listener(document_target, "mouseenter");
add_listener(window_target, "blur");
add_listener(document_target, "visibilitychange");
handles
}
fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) {
if let Some(document) = browser_window.document()
&& let Some(body) = document.body()
&& let Err(error) = body.style().set_property("cursor", css_cursor)
{
log::warn!("Failed to set cursor style: {error:?}");
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,56 +1,56 @@
// --- subpixel sprites --- //
struct SubpixelSprite {
order: u32,
pad: u32,
bounds: Bounds,
content_mask: Bounds,
color: Hsla,
tile: AtlasTile,
transformation: TransformationMatrix,
}
@group(1) @binding(0) var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
struct SubpixelSpriteOutput {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
@location(3) clip_distances: vec4<f32>,
}
struct SubpixelSpriteFragmentOutput {
@location(0) @blend_src(0) foreground: vec4<f32>,
@location(0) @blend_src(1) alpha: vec4<f32>,
}
@vertex
fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let sprite = b_subpixel_sprites[instance_id];
var out = SubpixelSpriteOutput();
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
return out;
}
@fragment
fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
var sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
if (gamma_params.is_bgr != 0u) {
sample = sample.bgr;
}
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
// Alpha clip after using the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
}
var out = SubpixelSpriteFragmentOutput();
out.foreground = vec4<f32>(input.color.rgb, 1.0);
out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
return out;
}
// --- subpixel sprites --- //
struct SubpixelSprite {
order: u32,
pad: u32,
bounds: Bounds,
content_mask: Bounds,
color: Hsla,
tile: AtlasTile,
transformation: TransformationMatrix,
}
@group(1) @binding(0) var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
struct SubpixelSpriteOutput {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
@location(3) clip_distances: vec4<f32>,
}
struct SubpixelSpriteFragmentOutput {
@location(0) @blend_src(0) foreground: vec4<f32>,
@location(0) @blend_src(1) alpha: vec4<f32>,
}
@vertex
fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let sprite = b_subpixel_sprites[instance_id];
var out = SubpixelSpriteOutput();
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
return out;
}
@fragment
fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
var sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
if (gamma_params.is_bgr != 0u) {
sample = sample.bgr;
}
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
// Alpha clip after using the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
}
var out = SubpixelSpriteFragmentOutput();
out.foreground = vec4<f32>(input.color.rgb, 1.0);
out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
return out;
}

View file

@ -1,101 +1,101 @@
use super::register_zed_scheme;
use anyhow::{Context as _, Result};
use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
use release_channel::ReleaseChannel;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use util::ResultExt;
use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::{Toast, Workspace};
actions!(
cli,
[
/// Installs the Zed CLI tool to the system PATH.
InstallCliBinary,
]
);
async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?;
let link_path = Path::new("/usr/local/bin/zed");
let bin_dir_path = link_path.parent().unwrap();
// Don't re-create symlink if it points to the same CLI binary.
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
return Ok(link_path.into());
}
// If the symlink is not there or is outdated, first try replacing it
// without escalating.
smol::fs::remove_file(link_path).await.log_err();
if smol::fs::unix::symlink(&cli_path, link_path)
.await
.log_err()
.is_some()
{
return Ok(link_path.into());
}
// The symlink could not be created, so use osascript with admin privileges
// to create it.
let status = smol::process::Command::new("/usr/bin/osascript")
.args([
"-e",
&format!(
"do shell script \" \
mkdir -p \'{}\' && \
ln -sf \'{}\' \'{}\' \
\" with administrator privileges",
bin_dir_path.to_string_lossy(),
cli_path.to_string_lossy(),
link_path.to_string_lossy(),
),
])
.stdout(smol::process::Stdio::inherit())
.stderr(smol::process::Stdio::inherit())
.output()
.await?
.status;
anyhow::ensure!(status.success(), "error running osascript");
Ok(link_path.into())
}
pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {
const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
cx.spawn_in(window, async move |workspace, cx| {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
let prompt = cx.prompt(
PromptLevel::Warning,
"CLI should already be installed",
Some(LINUX_PROMPT_DETAIL),
&["Ok"],
);
cx.background_spawn(prompt).detach();
return Ok(());
}
let path = install_script(cx.deref())
.await
.context("error creating CLI symlink")?;
workspace.update_in(cx, |workspace, _, cx| {
struct InstalledZedCli;
workspace.show_toast(
Toast::new(
NotificationId::unique::<InstalledZedCli>(),
format!(
"Installed `zed` to {}. You can launch {} from your terminal.",
path.to_string_lossy(),
ReleaseChannel::global(cx).display_name()
),
),
cx,
)
})?;
register_zed_scheme(cx).await.log_err();
Ok(())
})
.detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
}
use super::register_zed_scheme;
use anyhow::{Context as _, Result};
use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
use release_channel::ReleaseChannel;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use util::ResultExt;
use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::{Toast, Workspace};
actions!(
cli,
[
/// Installs the Zed CLI tool to the system PATH.
InstallCliBinary,
]
);
async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))?;
let link_path = Path::new("/usr/local/bin/zed");
let bin_dir_path = link_path.parent().unwrap();
// Don't re-create symlink if it points to the same CLI binary.
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
return Ok(link_path.into());
}
// If the symlink is not there or is outdated, first try replacing it
// without escalating.
smol::fs::remove_file(link_path).await.log_err();
if smol::fs::unix::symlink(&cli_path, link_path)
.await
.log_err()
.is_some()
{
return Ok(link_path.into());
}
// The symlink could not be created, so use osascript with admin privileges
// to create it.
let status = smol::process::Command::new("/usr/bin/osascript")
.args([
"-e",
&format!(
"do shell script \" \
mkdir -p \'{}\' && \
ln -sf \'{}\' \'{}\' \
\" with administrator privileges",
bin_dir_path.to_string_lossy(),
cli_path.to_string_lossy(),
link_path.to_string_lossy(),
),
])
.stdout(smol::process::Stdio::inherit())
.stderr(smol::process::Stdio::inherit())
.output()
.await?
.status;
anyhow::ensure!(status.success(), "error running osascript");
Ok(link_path.into())
}
pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {
const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
cx.spawn_in(window, async move |workspace, cx| {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
let prompt = cx.prompt(
PromptLevel::Warning,
"CLI should already be installed",
Some(LINUX_PROMPT_DETAIL),
&["Ok"],
);
cx.background_spawn(prompt).detach();
return Ok(());
}
let path = install_script(cx.deref())
.await
.context("error creating CLI symlink")?;
workspace.update_in(cx, |workspace, _, cx| {
struct InstalledZedCli;
workspace.show_toast(
Toast::new(
NotificationId::unique::<InstalledZedCli>(),
format!(
"Installed `zed` to {}. You can launch {} from your terminal.",
path.to_string_lossy(),
ReleaseChannel::global(cx).display_name()
),
),
cx,
)
})?;
register_zed_scheme(cx).await.log_err();
Ok(())
})
.detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
}

View file

@ -1,14 +1,14 @@
use client::ZED_URL_SCHEME;
use gpui::{AsyncApp, actions};
actions!(
cli,
[
/// Registers the zed:// URL scheme handler.
RegisterZedScheme
]
);
pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME)).await
}
use client::ZED_URL_SCHEME;
use gpui::{AsyncApp, actions};
actions!(
cli,
[
/// Registers the zed:// URL scheme handler.
RegisterZedScheme
]
);
pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME)).await
}

File diff suppressed because it is too large Load diff

View file

@ -1,342 +1,342 @@
//! Mock transport for testing remote connections.
//!
//! This module provides a mock implementation of the `RemoteConnection` trait
//! that allows testing remote editing functionality without actual SSH/WSL/Docker
//! connections.
//!
//! # Usage
//!
//! ```rust,ignore
//! use remote::{MockConnection, RemoteClient};
//!
//! #[gpui::test]
//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
//! let (opts, server_session) = MockConnection::new(cx, server_cx);
//!
//! // Create the headless project (server side)
//! server_cx.update(HeadlessProject::init);
//! let _headless = server_cx.new(|cx| {
//! HeadlessProject::new(
//! HeadlessAppState { session: server_session, /* ... */ },
//! false,
//! cx,
//! )
//! });
//!
//! // Create the client using the helper
//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await;
//! // ... test logic ...
//! }
//! ```
use crate::remote_client::{
ChannelClient, CommandTemplate, Interactive, RemoteClientDelegate, RemoteConnection,
RemoteConnectionOptions,
};
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use futures::{
FutureExt, SinkExt, StreamExt,
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased,
};
use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext};
use rpc::{AnyProtoClient, proto::Envelope};
use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use util::paths::{PathStyle, RemotePathBuf};
/// Unique identifier for a mock connection.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MockConnectionOptions {
pub id: u64,
}
/// A mock implementation of `RemoteConnection` for testing.
pub struct MockRemoteConnection {
options: MockConnectionOptions,
server_channel: Arc<ChannelClient>,
server_cx: SendableCx,
}
/// Wrapper to pass `AsyncApp` across thread boundaries in tests.
///
/// # Safety
///
/// This is safe because in test mode, GPUI is always single-threaded and so
/// having access to one async app means being on the same main thread.
pub(crate) struct SendableCx(AsyncApp);
impl SendableCx {
pub(crate) fn new(cx: &TestAppContext) -> Self {
Self(cx.to_async())
}
pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp {
self.0.clone()
}
}
// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx
// is only accessed from the main thread via the get() method which
// requires a valid AsyncApp reference.
unsafe impl Send for SendableCx {}
unsafe impl Sync for SendableCx {}
/// Global registry that holds pre-created mock connections.
///
/// When `ConnectionPool::connect` is called with `MockConnectionOptions`,
/// it retrieves the connection from this registry.
#[derive(Default)]
pub struct MockConnectionRegistry {
pending: HashMap<u64, (oneshot::Receiver<()>, Arc<MockRemoteConnection>)>,
}
impl Global for MockConnectionRegistry {}
impl MockConnectionRegistry {
/// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection.
pub fn take(
&mut self,
opts: &MockConnectionOptions,
) -> Option<impl Future<Output = Arc<MockRemoteConnection>> + use<>> {
let (guard, con) = self.pending.remove(&opts.id)?;
Some(async move {
_ = guard.await;
con
})
}
}
/// Helper for creating mock connection pairs in tests.
pub struct MockConnection;
pub type ConnectGuard = oneshot::Sender<()>;
impl MockConnection {
/// Creates a new mock connection pair for testing.
///
/// This function:
/// 1. Creates a unique `MockConnectionOptions` identifier
/// 2. Sets up the server-side channel (returned as `AnyProtoClient`)
/// 3. Creates a `MockRemoteConnection` and registers it in the global registry
/// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called
///
/// Returns:
/// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation
/// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session
///
/// # Arguments
/// - `client_cx`: The test context for the client side
/// - `server_cx`: The test context for the server/headless side
pub(crate) fn new(
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let opts = MockConnectionOptions { id };
let (server_client, connect_guard) =
Self::new_with_opts(opts.clone(), client_cx, server_cx);
(opts, server_client, connect_guard)
}
/// Creates a mock connection pair for existing `MockConnectionOptions`.
///
/// This is useful when simulating reconnection: after a connection is torn
/// down, register a new mock server under the same options so the next
/// `ConnectionPool::connect` call finds it.
pub(crate) fn new_with_opts(
opts: MockConnectionOptions,
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (AnyProtoClient, ConnectGuard) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
let server_client = server_cx
.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false));
let connection = Arc::new(MockRemoteConnection {
options: opts.clone(),
server_channel: server_client.clone(),
server_cx: SendableCx::new(server_cx),
});
let (tx, rx) = oneshot::channel();
client_cx.update(|cx| {
cx.default_global::<MockConnectionRegistry>()
.pending
.insert(opts.id, (rx, connection));
});
(server_client.into(), tx)
}
}
#[async_trait(?Send)]
impl RemoteConnection for MockRemoteConnection {
async fn kill(&self) -> Result<()> {
Ok(())
}
fn has_been_killed(&self) -> bool {
false
}
fn build_command(
&self,
program: Option<String>,
args: &[String],
env: &HashMap<String, String>,
_working_dir: Option<String>,
_port_forward: Option<(u16, String, u16)>,
_interactive: Interactive,
) -> Result<CommandTemplate> {
let shell_program = program.unwrap_or_else(|| "sh".to_string());
let mut shell_args = Vec::new();
shell_args.push(shell_program);
shell_args.extend(args.iter().cloned());
Ok(CommandTemplate {
program: "mock".into(),
args: shell_args,
env: env.clone(),
})
}
fn build_forward_ports_command(
&self,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
Ok(CommandTemplate {
program: "mock".into(),
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}
fn upload_directory(
&self,
_src_path: PathBuf,
_dest_path: RemotePathBuf,
_cx: &App,
) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Mock(self.options.clone())
}
fn simulate_disconnect(&self, cx: &AsyncApp) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
self.server_channel
.reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
}
fn start_proxy(
&self,
_unique_identifier: String,
_reconnect: bool,
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
_delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
self.server_channel.reconnect(
server_incoming_rx,
server_outgoing_tx,
&self.server_cx.get(cx),
);
cx.background_spawn(async move {
loop {
select_biased! {
server_to_client = server_outgoing_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(1)
};
connection_activity_tx.try_send(()).ok();
client_incoming_tx.send(server_to_client).await.ok();
}
client_to_server = client_outgoing_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(1)
};
server_incoming_tx.send(client_to_server).await.ok();
}
}
}
})
}
fn path_style(&self) -> PathStyle {
PathStyle::local()
}
fn shell(&self) -> String {
"sh".to_owned()
}
fn default_system_shell(&self) -> String {
"sh".to_owned()
}
fn has_wsl_interop(&self) -> bool {
false
}
}
/// Mock delegate for tests that don't need delegate functionality.
pub struct MockDelegate;
impl RemoteClientDelegate for MockDelegate {
fn ask_password(
&self,
_prompt: String,
_sender: futures::channel::oneshot::Sender<askpass::EncryptedPassword>,
_cx: &mut AsyncApp,
) {
unreachable!("MockDelegate::ask_password should not be called in tests")
}
fn download_server_binary_locally(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<PathBuf>> {
unreachable!("MockDelegate::download_server_binary_locally should not be called in tests")
}
fn get_download_url(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<Option<String>>> {
unreachable!("MockDelegate::get_download_url should not be called in tests")
}
fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {}
}
//! Mock transport for testing remote connections.
//!
//! This module provides a mock implementation of the `RemoteConnection` trait
//! that allows testing remote editing functionality without actual SSH/WSL/Docker
//! connections.
//!
//! # Usage
//!
//! ```rust,ignore
//! use remote::{MockConnection, RemoteClient};
//!
//! #[gpui::test]
//! async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
//! let (opts, server_session) = MockConnection::new(cx, server_cx);
//!
//! // Create the headless project (server side)
//! server_cx.update(HeadlessProject::init);
//! let _headless = server_cx.new(|cx| {
//! HeadlessProject::new(
//! HeadlessAppState { session: server_session, /* ... */ },
//! false,
//! cx,
//! )
//! });
//!
//! // Create the client using the helper
//! let (client, server_client) = RemoteClient::new_mock(cx, server_cx).await;
//! // ... test logic ...
//! }
//! ```
use crate::remote_client::{
ChannelClient, CommandTemplate, Interactive, RemoteClientDelegate, RemoteConnection,
RemoteConnectionOptions,
};
use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use futures::{
FutureExt, SinkExt, StreamExt,
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased,
};
use gpui::{App, AppContext as _, AsyncApp, Global, Task, TestAppContext};
use rpc::{AnyProtoClient, proto::Envelope};
use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use util::paths::{PathStyle, RemotePathBuf};
/// Unique identifier for a mock connection.
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MockConnectionOptions {
pub id: u64,
}
/// A mock implementation of `RemoteConnection` for testing.
pub struct MockRemoteConnection {
options: MockConnectionOptions,
server_channel: Arc<ChannelClient>,
server_cx: SendableCx,
}
/// Wrapper to pass `AsyncApp` across thread boundaries in tests.
///
/// # Safety
///
/// This is safe because in test mode, GPUI is always single-threaded and so
/// having access to one async app means being on the same main thread.
pub(crate) struct SendableCx(AsyncApp);
impl SendableCx {
pub(crate) fn new(cx: &TestAppContext) -> Self {
Self(cx.to_async())
}
pub(crate) fn get(&self, _: &AsyncApp) -> AsyncApp {
self.0.clone()
}
}
// SAFETY: In test mode, GPUI is always single-threaded, and SendableCx
// is only accessed from the main thread via the get() method which
// requires a valid AsyncApp reference.
unsafe impl Send for SendableCx {}
unsafe impl Sync for SendableCx {}
/// Global registry that holds pre-created mock connections.
///
/// When `ConnectionPool::connect` is called with `MockConnectionOptions`,
/// it retrieves the connection from this registry.
#[derive(Default)]
pub struct MockConnectionRegistry {
pending: HashMap<u64, (oneshot::Receiver<()>, Arc<MockRemoteConnection>)>,
}
impl Global for MockConnectionRegistry {}
impl MockConnectionRegistry {
/// Called by `ConnectionPool::connect` to retrieve a pre-registered mock connection.
pub fn take(
&mut self,
opts: &MockConnectionOptions,
) -> Option<impl Future<Output = Arc<MockRemoteConnection>> + use<>> {
let (guard, con) = self.pending.remove(&opts.id)?;
Some(async move {
_ = guard.await;
con
})
}
}
/// Helper for creating mock connection pairs in tests.
pub struct MockConnection;
pub type ConnectGuard = oneshot::Sender<()>;
impl MockConnection {
/// Creates a new mock connection pair for testing.
///
/// This function:
/// 1. Creates a unique `MockConnectionOptions` identifier
/// 2. Sets up the server-side channel (returned as `AnyProtoClient`)
/// 3. Creates a `MockRemoteConnection` and registers it in the global registry
/// 4. The connection will be retrieved from the registry when `ConnectionPool::connect` is called
///
/// Returns:
/// - `MockConnectionOptions` to pass to `remote::connect()` or `RemoteClient` creation
/// - `AnyProtoClient` to pass to `HeadlessProject::new()` as the session
///
/// # Arguments
/// - `client_cx`: The test context for the client side
/// - `server_cx`: The test context for the server/headless side
pub(crate) fn new(
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (MockConnectionOptions, AnyProtoClient, ConnectGuard) {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let opts = MockConnectionOptions { id };
let (server_client, connect_guard) =
Self::new_with_opts(opts.clone(), client_cx, server_cx);
(opts, server_client, connect_guard)
}
/// Creates a mock connection pair for existing `MockConnectionOptions`.
///
/// This is useful when simulating reconnection: after a connection is torn
/// down, register a new mock server under the same options so the next
/// `ConnectionPool::connect` call finds it.
pub(crate) fn new_with_opts(
opts: MockConnectionOptions,
client_cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (AnyProtoClient, ConnectGuard) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
let server_client = server_cx
.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "mock-server", false));
let connection = Arc::new(MockRemoteConnection {
options: opts.clone(),
server_channel: server_client.clone(),
server_cx: SendableCx::new(server_cx),
});
let (tx, rx) = oneshot::channel();
client_cx.update(|cx| {
cx.default_global::<MockConnectionRegistry>()
.pending
.insert(opts.id, (rx, connection));
});
(server_client.into(), tx)
}
}
#[async_trait(?Send)]
impl RemoteConnection for MockRemoteConnection {
async fn kill(&self) -> Result<()> {
Ok(())
}
fn has_been_killed(&self) -> bool {
false
}
fn build_command(
&self,
program: Option<String>,
args: &[String],
env: &HashMap<String, String>,
_working_dir: Option<String>,
_port_forward: Option<(u16, String, u16)>,
_interactive: Interactive,
) -> Result<CommandTemplate> {
let shell_program = program.unwrap_or_else(|| "sh".to_string());
let mut shell_args = Vec::new();
shell_args.push(shell_program);
shell_args.extend(args.iter().cloned());
Ok(CommandTemplate {
program: "mock".into(),
args: shell_args,
env: env.clone(),
})
}
fn build_forward_ports_command(
&self,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
Ok(CommandTemplate {
program: "mock".into(),
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}
fn upload_directory(
&self,
_src_path: PathBuf,
_dest_path: RemotePathBuf,
_cx: &App,
) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Mock(self.options.clone())
}
fn simulate_disconnect(&self, cx: &AsyncApp) {
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
self.server_channel
.reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx));
}
fn start_proxy(
&self,
_unique_identifier: String,
_reconnect: bool,
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
_delegate: Arc<dyn RemoteClientDelegate>,
cx: &mut AsyncApp,
) -> Task<Result<i32>> {
let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
self.server_channel.reconnect(
server_incoming_rx,
server_outgoing_tx,
&self.server_cx.get(cx),
);
cx.background_spawn(async move {
loop {
select_biased! {
server_to_client = server_outgoing_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(1)
};
connection_activity_tx.try_send(()).ok();
client_incoming_tx.send(server_to_client).await.ok();
}
client_to_server = client_outgoing_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(1)
};
server_incoming_tx.send(client_to_server).await.ok();
}
}
}
})
}
fn path_style(&self) -> PathStyle {
PathStyle::local()
}
fn shell(&self) -> String {
"sh".to_owned()
}
fn default_system_shell(&self) -> String {
"sh".to_owned()
}
fn has_wsl_interop(&self) -> bool {
false
}
}
/// Mock delegate for tests that don't need delegate functionality.
pub struct MockDelegate;
impl RemoteClientDelegate for MockDelegate {
fn ask_password(
&self,
_prompt: String,
_sender: futures::channel::oneshot::Sender<askpass::EncryptedPassword>,
_cx: &mut AsyncApp,
) {
unreachable!("MockDelegate::ask_password should not be called in tests")
}
fn download_server_binary_locally(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<PathBuf>> {
unreachable!("MockDelegate::download_server_binary_locally should not be called in tests")
}
fn get_download_url(
&self,
_platform: crate::RemotePlatform,
_release_channel: release_channel::ReleaseChannel,
_version: Option<semver::Version>,
_cx: &mut AsyncApp,
) -> Task<Result<Option<String>>> {
unreachable!("MockDelegate::get_download_url should not be called in tests")
}
fn set_status(&self, _status: Option<&str>, _cx: &mut AsyncApp) {}
}