mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
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:
parent
63ff99795c
commit
1dac14c53a
12 changed files with 3365 additions and 3365 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue