Vectrum
Sign In

Open diffs of remote threads by asking their host

JjohannSocommitted 1 parent f8f97f5commit4181e0a

Showing 4 changed files with 116 additions and 21 deletions.

+40−3src/remote/client.rsView File
@@ -7,11 +7,12 @@
77 //! slower one it keeps trying the faster ones and moves as soon as one works.
88
99 use std::{
10+ collections::HashMap,
1011 io::ErrorKind,
1112 net::{TcpStream, ToSocketAddrs},
1213 sync::{
1314 Arc, Mutex,
14- atomic::{AtomicBool, Ordering},
15+ atomic::{AtomicBool, AtomicU64, Ordering},
1516 mpsc,
1617 },
1718 thread,
@@ -20,7 +21,9 @@ use std::{
2021
2122 use tungstenite::{Message, WebSocket};
2223
23- use super::{ClientMessage, DEFAULT_PORT, PROTOCOL, Route, RouteKind, ServerMessage, tailnet_peers, tailnet_routes};
24+ use super::{
25+ ClientMessage, DEFAULT_PORT, PROTOCOL, RemoteFileDiff, Route, RouteKind, ServerMessage, tailnet_peers, tailnet_routes,
26+ };
2427
2528 /// How long to wait for a route that goes over the internet or a VPN.
2629 const CONNECT_TIMEOUT: Duration = Duration::from_secs(6);
@@ -65,6 +68,9 @@ pub struct RemoteClient {
6568 stop: Arc<AtomicBool>,
6669 routes: Arc<Mutex<Vec<Route>>>,
6770 pub events: smol::channel::Receiver<ClientEvent>,
71+ /// Diffs asked for and not answered yet, by request.
72+ diffs: Mutex<HashMap<u64, smol::channel::Sender<Result<Vec<RemoteFileDiff>, String>>>>,
73+ next_request: AtomicU64,
6874 }
6975
7076 /// A way to the host that works off the UI thread.
@@ -138,7 +144,38 @@ impl RemoteClient {
138144 }
139145 });
140146 }
141- Self { outbox, stop, routes, events }
147+ Self { outbox, stop, routes, events, diffs: Mutex::default(), next_request: AtomicU64::new(1) }
148+ }
149+
150+ /// Asks the host for the diff between two of a thread's snapshots. The
151+ /// answer arrives on the returned channel once [`Self::resolve_diff`]
152+ /// gets it; older hosts never answer.
153+ pub fn request_diff(
154+ &self,
155+ thread: String,
156+ base: String,
157+ head: String,
158+ path: Option<String>,
159+ ignore_whitespace: bool,
160+ ) -> (u64, smol::channel::Receiver<Result<Vec<RemoteFileDiff>, String>>) {
161+ let request = self.next_request.fetch_add(1, Ordering::Relaxed);
162+ let (tx, rx) = smol::channel::bounded(1);
163+ self.diffs.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(request, tx);
164+ self.send(&ClientMessage::Diff { request, thread, base, head, path, ignore_whitespace });
165+ (request, rx)
166+ }
167+
168+ /// Hands a diff reply to the request waiting for it.
169+ pub fn resolve_diff(&self, request: u64, result: Result<Vec<RemoteFileDiff>, String>) {
170+ let waiting = self.diffs.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&request);
171+ if let Some(waiting) = waiting {
172+ let _ = waiting.try_send(result);
173+ }
174+ }
175+
176+ /// Forgets a request that stopped waiting.
177+ pub fn forget_diff(&self, request: u64) {
178+ self.diffs.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).remove(&request);
142179 }
143180
144181 /// Changes the routes without dropping the connection; the next try,
+25−0src/remote/mod.rsView File
@@ -436,6 +436,27 @@ impl From<&FileDiff> for RemoteFileDiff {
436436 }
437437 }
438438
439+ impl From<RemoteFileDiff> for FileDiff {
440+ fn from(file: RemoteFileDiff) -> Self {
441+ let lines = file
442+ .lines
443+ .into_iter()
444+ .map(|line| crate::git::DiffLine {
445+ kind: match line.kind.as_str() {
446+ "hunk" => LineKind::Hunk,
447+ "added" => LineKind::Added,
448+ "removed" => LineKind::Removed,
449+ _ => LineKind::Context,
450+ },
451+ text: line.text,
452+ old_line: line.old_line,
453+ new_line: line.new_line,
454+ })
455+ .collect();
456+ Self { path: file.path, binary: file.binary, lines }
457+ }
458+ }
459+
439460 /// What kind of network a route goes over, which says how fast it likely is.
440461 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
441462 #[serde(rename_all = "snake_case")]
@@ -1029,6 +1050,10 @@ mod tests {
10291050 ServerMessage::DiffReply { request: 6, files: received, error: None } => assert_eq!(received, files),
10301051 _ => panic!("not a diff reply"),
10311052 }
1053+ let back = crate::git::FileDiff::from(files[0].clone());
1054+ assert_eq!((back.path.as_str(), back.binary, back.lines.len()), ("src/a.rs", false, 2));
1055+ assert_eq!((back.lines[0].kind, back.lines[1].kind), (LineKind::Hunk, LineKind::Added));
1056+ assert_eq!((back.lines[1].text.as_str(), back.lines[1].new_line), ("new", Some(1)));
10321057 }
10331058
10341059 #[::core::prelude::v1::test]
+45−17src/ui/chat.rsView File
@@ -82,6 +82,8 @@ const PROACTIVE_FILES: usize = 3;
8282 const PROACTIVE_LINES: u64 = 50;
8383 /// The most rows the `/`, `$` and `@` menus list.
8484 const MENU_ROWS: usize = 40;
85+ /// How long a remote thread's diff may take to come back from its host.
86+ const REMOTE_DIFF_TIMEOUT: Duration = Duration::from_secs(30);
8587
8688 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
8789 enum Picker {
@@ -1265,36 +1267,62 @@ impl ChatView {
12651267 let Some(diff) = &self.diff else { return };
12661268 let session = self.session.read(cx);
12671269 let Some(changes) = session.turn_changes(diff.end) else { return };
1268- if session.is_remote() {
1269- // The snapshots live in the host's repository.
1270- let note = Err("This thread runs on another device; its diffs open there.".to_string());
1271- if let Some(diff) = self.diff.as_mut() {
1272- diff.set_files(note);
1273- }
1270+ let (base, head) = (changes.base.clone(), changes.head.clone());
1271+ let (end, path, ignore_whitespace) = (diff.end, diff.path.clone(), diff.ignore_whitespace);
1272+ if let Some(remote) = &session.remote {
1273+ // The snapshots live in the host's repository: ask it.
1274+ let Some(thread) = remote.thread.clone() else { return };
1275+ let client = remote.client.clone();
1276+ let (request, reply) = client.request_diff(thread, base, head, path.clone(), ignore_whitespace);
1277+ let timeout = cx.background_executor().timer(REMOTE_DIFF_TIMEOUT);
1278+ cx.spawn_in(window, async move |this, cx| {
1279+ let answer = smol::future::or(async { reply.recv().await.ok() }, async {
1280+ timeout.await;
1281+ None
1282+ })
1283+ .await;
1284+ let files = match answer {
1285+ Some(result) => result.map(|files| files.into_iter().map(git::FileDiff::from).collect()),
1286+ None => {
1287+ client.forget_diff(request);
1288+ Err("The other device didn't answer. Its Vectrum Code may be too old to share diffs, or the connection dropped."
1289+ .to_string())
1290+ }
1291+ };
1292+ let _ = this.update(cx, |this, cx| this.diff_loaded(end, &path, ignore_whitespace, files, cx));
1293+ })
1294+ .detach();
12741295 return;
12751296 }
1276- let (base, head) = (changes.base.clone(), changes.head.clone());
12771297 let cwd = session.cwd.clone();
1278- let (end, path, ignore_whitespace) = (diff.end, diff.path.clone(), diff.ignore_whitespace);
12791298 let task = cx.background_executor().spawn({
12801299 let path = path.clone();
12811300 async move { git::diff_between(&cwd, &base, &head, path.as_deref(), ignore_whitespace) }
12821301 });
12831302 cx.spawn_in(window, async move |this, cx| {
12841303 let files = task.await;
1285- let _ = this.update(cx, |this, cx| {
1286- // Unless the diff was closed, replaced or toggled meanwhile.
1287- if let Some(diff) = this.diff.as_mut()
1288- && (diff.end, &diff.path, diff.ignore_whitespace) == (end, &path, ignore_whitespace)
1289- {
1290- diff.set_files(files);
1291- cx.notify();
1292- }
1293- });
1304+ let _ = this.update(cx, |this, cx| this.diff_loaded(end, &path, ignore_whitespace, files, cx));
12941305 })
12951306 .detach();
12961307 }
12971308
1309+ /// Shows a diff that was read, unless it was closed, replaced or toggled meanwhile.
1310+ fn diff_loaded(
1311+ &mut self,
1312+ end: usize,
1313+ path: &Option<String>,
1314+ ignore_whitespace: bool,
1315+ files: Result<Vec<git::FileDiff>, String>,
1316+ cx: &mut Context<Self>,
1317+ ) {
1318+ if let Some(diff) = self.diff.as_mut()
1319+ && (diff.end, &diff.path, diff.ignore_whitespace) == (end, path, ignore_whitespace)
1320+ {
1321+ diff.set_files(files);
1322+ cx.notify();
1323+ }
1324+ }
1325+
12981326 fn diff_action(&mut self, action: DiffAction, window: &mut Window, cx: &mut Context<Self>) {
12991327 let Some(diff) = self.diff.as_mut() else { return };
13001328 match action {
+6−1src/ui/workspace/remote.rsView File
@@ -847,7 +847,12 @@ impl Workspace {
847847 ClientEvent::Message(ServerMessage::Routes { routes }) => self.learn_routes(env, &routes),
848848 ClientEvent::Message(ServerMessage::Welcome { .. } | ServerMessage::Denied { .. }) => {}
849849 // Only sent in reply to requests this app doesn't make.
850- ClientEvent::Message(ServerMessage::Catalog { .. } | ServerMessage::DiffReply { .. }) => {}
850+ ClientEvent::Message(ServerMessage::Catalog { .. }) => {}
851+ ClientEvent::Message(ServerMessage::DiffReply { request, files, error }) => {
852+ if let Some(environment) = self.remote.environments.iter().find(|candidate| candidate.host.id == env) {
853+ environment.client.resolve_diff(request, error.map_or(Ok(files), Err));
854+ }
855+ }
851856 ClientEvent::Message(ServerMessage::DeviceReply { request, ok, error }) => {
852857 if let Some(link) = self.devices_link(env) {
853858 link.resolve(request, error.map_or_else(|| Ok(ok.unwrap_or_default()), Err));