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        session.add_message_handler(buffer_store.downgrade(), BufferStore::handle_close_buffer);
 59
 60        session.add_request_handler(
 61            worktree_store.downgrade(),
 62            WorktreeStore::handle_create_project_entry,
 63        );
 64        session.add_request_handler(
 65            worktree_store.downgrade(),
 66            WorktreeStore::handle_rename_project_entry,
 67        );
 68        session.add_request_handler(
 69            worktree_store.downgrade(),
 70            WorktreeStore::handle_copy_project_entry,
 71        );
 72        session.add_request_handler(
 73            worktree_store.downgrade(),
 74            WorktreeStore::handle_delete_project_entry,
 75        );
 76        session.add_request_handler(
 77            worktree_store.downgrade(),
 78            WorktreeStore::handle_expand_project_entry,
 79        );
 80
 81        HeadlessProject {
 82            session: session.into(),
 83            fs,
 84            worktree_store,
 85            buffer_store,
 86            next_entry_id: Default::default(),
 87        }
 88    }
 89
 90    pub async fn handle_add_worktree(
 91        this: Model<Self>,
 92        message: TypedEnvelope<proto::AddWorktree>,
 93        mut cx: AsyncAppContext,
 94    ) -> Result<proto::AddWorktreeResponse> {
 95        let path = shellexpand::tilde(&message.payload.path).to_string();
 96        let worktree = this
 97            .update(&mut cx.clone(), |this, _| {
 98                Worktree::local(
 99                    Path::new(&path),
100                    true,
101                    this.fs.clone(),
102                    this.next_entry_id.clone(),
103                    &mut cx,
104                )
105            })?
106            .await?;
107
108        this.update(&mut cx, |this, cx| {
109            let session = this.session.clone();
110            this.worktree_store.update(cx, |worktree_store, cx| {
111                worktree_store.add(&worktree, cx);
112            });
113            worktree.update(cx, |worktree, cx| {
114                worktree.observe_updates(0, cx, move |update| {
115                    session.send(update).ok();
116                    futures::future::ready(true)
117                });
118                proto::AddWorktreeResponse {
119                    worktree_id: worktree.id().to_proto(),
120                }
121            })
122        })
123    }
124
125    pub async fn handle_open_buffer_by_path(
126        this: Model<Self>,
127        message: TypedEnvelope<proto::OpenBufferByPath>,
128        mut cx: AsyncAppContext,
129    ) -> Result<proto::OpenBufferResponse> {
130        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
131        let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
132            let buffer_store = this.buffer_store.clone();
133            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
134                buffer_store.open_buffer(
135                    ProjectPath {
136                        worktree_id,
137                        path: PathBuf::from(message.payload.path).into(),
138                    },
139                    cx,
140                )
141            });
142            anyhow::Ok((buffer_store, buffer, this.session.clone()))
143        })??;
144
145        let buffer = buffer.await?;
146        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
147        buffer_store.update(&mut cx, |buffer_store, cx| {
148            buffer_store
149                .create_buffer_for_peer(&buffer, PEER_ID, PROJECT_ID, session, cx)
150                .detach_and_log_err(cx);
151        })?;
152
153        Ok(proto::OpenBufferResponse {
154            buffer_id: buffer_id.to_proto(),
155        })
156    }
157
158    pub async fn handle_find_search_candidates(
159        this: Model<Self>,
160        envelope: TypedEnvelope<proto::FindSearchCandidates>,
161        mut cx: AsyncAppContext,
162    ) -> Result<proto::FindSearchCandidatesResponse> {
163        let message = envelope.payload;
164        let query = SearchQuery::from_proto(
165            message
166                .query
167                .ok_or_else(|| anyhow!("missing query field"))?,
168        )?;
169        let mut results = this.update(&mut cx, |this, cx| {
170            this.buffer_store.update(cx, |buffer_store, cx| {
171                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
172            })
173        })?;
174
175        let mut response = proto::FindSearchCandidatesResponse {
176            buffer_ids: Vec::new(),
177        };
178
179        let (buffer_store, client) = this.update(&mut cx, |this, _| {
180            (this.buffer_store.clone(), this.session.clone())
181        })?;
182
183        while let Some(buffer) = results.next().await {
184            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
185            response.buffer_ids.push(buffer_id.to_proto());
186            buffer_store
187                .update(&mut cx, |buffer_store, cx| {
188                    buffer_store.create_buffer_for_peer(
189                        &buffer,
190                        PEER_ID,
191                        PROJECT_ID,
192                        client.clone(),
193                        cx,
194                    )
195                })?
196                .await?;
197        }
198
199        Ok(response)
200    }
201
202    pub async fn handle_list_remote_directory(
203        this: Model<Self>,
204        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
205        cx: AsyncAppContext,
206    ) -> Result<proto::ListRemoteDirectoryResponse> {
207        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
208        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
209
210        let mut entries = Vec::new();
211        let mut response = fs.read_dir(Path::new(&expanded)).await?;
212        while let Some(path) = response.next().await {
213            if let Some(file_name) = path?.file_name() {
214                entries.push(file_name.to_string_lossy().to_string());
215            }
216        }
217        Ok(proto::ListRemoteDirectoryResponse { entries })
218    }
219
220    pub fn on_buffer_store_event(
221        &mut self,
222        _: Model<BufferStore>,
223        event: &BufferStoreEvent,
224        _: &mut ModelContext<Self>,
225    ) {
226        match event {
227            BufferStoreEvent::MessageToReplicas(message) => {
228                self.session
229                    .send_dynamic(message.as_ref().clone())
230                    .log_err();
231            }
232            _ => {}
233        }
234    }
235}