1use anyhow::Result;
2use fs::Fs;
3use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
4use project::{
5 buffer_store::{BufferStore, BufferStoreEvent},
6 worktree_store::WorktreeStore,
7 ProjectPath, WorktreeId, WorktreeSettings,
8};
9use remote::SshSession;
10use rpc::{
11 proto::{self, AnyProtoClient, PeerId},
12 TypedEnvelope,
13};
14use settings::{Settings as _, SettingsStore};
15use smol::stream::StreamExt;
16use std::{
17 path::{Path, PathBuf},
18 sync::{atomic::AtomicUsize, Arc},
19};
20use util::ResultExt as _;
21use worktree::Worktree;
22
23const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
24const PROJECT_ID: u64 = 0;
25
26pub struct HeadlessProject {
27 pub fs: Arc<dyn Fs>,
28 pub session: AnyProtoClient,
29 pub worktree_store: Model<WorktreeStore>,
30 pub buffer_store: Model<BufferStore>,
31 pub next_entry_id: Arc<AtomicUsize>,
32}
33
34impl HeadlessProject {
35 pub fn init(cx: &mut AppContext) {
36 cx.set_global(SettingsStore::new(cx));
37 WorktreeSettings::register(cx);
38 }
39
40 pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
41 let this = cx.weak_model();
42
43 let worktree_store = cx.new_model(|_| WorktreeStore::new(true));
44 let buffer_store =
45 cx.new_model(|cx| BufferStore::new(worktree_store.clone(), Some(PROJECT_ID), cx));
46 cx.subscribe(&buffer_store, Self::on_buffer_store_event)
47 .detach();
48
49 session.add_request_handler(this.clone(), Self::handle_list_remote_directory);
50 session.add_request_handler(this.clone(), Self::handle_add_worktree);
51 session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
52
53 session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_blame_buffer);
54 session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_update_buffer);
55 session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_save_buffer);
56
57 session.add_request_handler(
58 worktree_store.downgrade(),
59 WorktreeStore::handle_create_project_entry,
60 );
61 session.add_request_handler(
62 worktree_store.downgrade(),
63 WorktreeStore::handle_rename_project_entry,
64 );
65 session.add_request_handler(
66 worktree_store.downgrade(),
67 WorktreeStore::handle_copy_project_entry,
68 );
69 session.add_request_handler(
70 worktree_store.downgrade(),
71 WorktreeStore::handle_delete_project_entry,
72 );
73 session.add_request_handler(
74 worktree_store.downgrade(),
75 WorktreeStore::handle_expand_project_entry,
76 );
77
78 HeadlessProject {
79 session: session.into(),
80 fs,
81 worktree_store,
82 buffer_store,
83 next_entry_id: Default::default(),
84 }
85 }
86
87 pub async fn handle_add_worktree(
88 this: Model<Self>,
89 message: TypedEnvelope<proto::AddWorktree>,
90 mut cx: AsyncAppContext,
91 ) -> Result<proto::AddWorktreeResponse> {
92 let path = shellexpand::tilde(&message.payload.path).to_string();
93 let worktree = this
94 .update(&mut cx.clone(), |this, _| {
95 Worktree::local(
96 Path::new(&path),
97 true,
98 this.fs.clone(),
99 this.next_entry_id.clone(),
100 &mut cx,
101 )
102 })?
103 .await?;
104
105 this.update(&mut cx, |this, cx| {
106 let session = this.session.clone();
107 this.worktree_store.update(cx, |worktree_store, cx| {
108 worktree_store.add(&worktree, cx);
109 });
110 worktree.update(cx, |worktree, cx| {
111 worktree.observe_updates(0, cx, move |update| {
112 session.send(update).ok();
113 futures::future::ready(true)
114 });
115 proto::AddWorktreeResponse {
116 worktree_id: worktree.id().to_proto(),
117 }
118 })
119 })
120 }
121
122 pub async fn handle_open_buffer_by_path(
123 this: Model<Self>,
124 message: TypedEnvelope<proto::OpenBufferByPath>,
125 mut cx: AsyncAppContext,
126 ) -> Result<proto::OpenBufferResponse> {
127 let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
128 let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
129 let buffer_store = this.buffer_store.clone();
130 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
131 buffer_store.open_buffer(
132 ProjectPath {
133 worktree_id,
134 path: PathBuf::from(message.payload.path).into(),
135 },
136 cx,
137 )
138 });
139 anyhow::Ok((buffer_store, buffer, this.session.clone()))
140 })??;
141
142 let buffer = buffer.await?;
143 let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
144
145 cx.spawn(|mut cx| async move {
146 BufferStore::create_buffer_for_peer(
147 buffer_store,
148 PEER_ID,
149 buffer_id,
150 PROJECT_ID,
151 session,
152 &mut cx,
153 )
154 .await
155 })
156 .detach();
157
158 Ok(proto::OpenBufferResponse {
159 buffer_id: buffer_id.to_proto(),
160 })
161 }
162
163 pub async fn handle_list_remote_directory(
164 this: Model<Self>,
165 envelope: TypedEnvelope<proto::ListRemoteDirectory>,
166 cx: AsyncAppContext,
167 ) -> Result<proto::ListRemoteDirectoryResponse> {
168 let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
169 let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
170
171 let mut entries = Vec::new();
172 let mut response = fs.read_dir(Path::new(&expanded)).await?;
173 while let Some(path) = response.next().await {
174 if let Some(file_name) = path?.file_name() {
175 entries.push(file_name.to_string_lossy().to_string());
176 }
177 }
178 Ok(proto::ListRemoteDirectoryResponse { entries })
179 }
180
181 pub fn on_buffer_store_event(
182 &mut self,
183 _: Model<BufferStore>,
184 event: &BufferStoreEvent,
185 _: &mut ModelContext<Self>,
186 ) {
187 match event {
188 BufferStoreEvent::MessageToReplicas(message) => {
189 self.session
190 .send_dynamic(message.as_ref().clone())
191 .log_err();
192 }
193 _ => {}
194 }
195 }
196}