headless_project.rs

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