headless_project.rs

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