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, SSH_PEER_ID, SSH_PROJECT_ID},
 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
 21pub struct HeadlessProject {
 22    pub fs: Arc<dyn Fs>,
 23    pub session: AnyProtoClient,
 24    pub worktree_store: Model<WorktreeStore>,
 25    pub buffer_store: Model<BufferStore>,
 26    pub next_entry_id: Arc<AtomicUsize>,
 27}
 28
 29impl HeadlessProject {
 30    pub fn init(cx: &mut AppContext) {
 31        cx.set_global(SettingsStore::new(cx));
 32        WorktreeSettings::register(cx);
 33    }
 34
 35    pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
 36        let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone()));
 37        let buffer_store = cx.new_model(|cx| {
 38            let mut buffer_store =
 39                BufferStore::new(worktree_store.clone(), Some(SSH_PROJECT_ID), cx);
 40            buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 41            buffer_store
 42        });
 43
 44        let client: AnyProtoClient = session.clone().into();
 45
 46        session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
 47        session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
 48        session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
 49
 50        client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
 51
 52        client.add_model_request_handler(Self::handle_add_worktree);
 53        client.add_model_request_handler(Self::handle_open_buffer_by_path);
 54        client.add_model_request_handler(Self::handle_find_search_candidates);
 55
 56        client.add_model_request_handler(BufferStore::handle_update_buffer);
 57        client.add_model_message_handler(BufferStore::handle_close_buffer);
 58
 59        BufferStore::init(&client);
 60        WorktreeStore::init(&client);
 61
 62        HeadlessProject {
 63            session: client,
 64            fs,
 65            worktree_store,
 66            buffer_store,
 67            next_entry_id: Default::default(),
 68        }
 69    }
 70
 71    pub async fn handle_add_worktree(
 72        this: Model<Self>,
 73        message: TypedEnvelope<proto::AddWorktree>,
 74        mut cx: AsyncAppContext,
 75    ) -> Result<proto::AddWorktreeResponse> {
 76        let path = shellexpand::tilde(&message.payload.path).to_string();
 77        let worktree = this
 78            .update(&mut cx.clone(), |this, _| {
 79                Worktree::local(
 80                    Path::new(&path),
 81                    true,
 82                    this.fs.clone(),
 83                    this.next_entry_id.clone(),
 84                    &mut cx,
 85                )
 86            })?
 87            .await?;
 88
 89        this.update(&mut cx, |this, cx| {
 90            let session = this.session.clone();
 91            this.worktree_store.update(cx, |worktree_store, cx| {
 92                worktree_store.add(&worktree, cx);
 93            });
 94            worktree.update(cx, |worktree, cx| {
 95                worktree.observe_updates(0, cx, move |update| {
 96                    session.send(update).ok();
 97                    futures::future::ready(true)
 98                });
 99                proto::AddWorktreeResponse {
100                    worktree_id: worktree.id().to_proto(),
101                }
102            })
103        })
104    }
105
106    pub async fn handle_open_buffer_by_path(
107        this: Model<Self>,
108        message: TypedEnvelope<proto::OpenBufferByPath>,
109        mut cx: AsyncAppContext,
110    ) -> Result<proto::OpenBufferResponse> {
111        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
112        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
113            let buffer_store = this.buffer_store.clone();
114            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
115                buffer_store.open_buffer(
116                    ProjectPath {
117                        worktree_id,
118                        path: PathBuf::from(message.payload.path).into(),
119                    },
120                    cx,
121                )
122            });
123            anyhow::Ok((buffer_store, buffer))
124        })??;
125
126        let buffer = buffer.await?;
127        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
128        buffer_store.update(&mut cx, |buffer_store, cx| {
129            buffer_store
130                .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
131                .detach_and_log_err(cx);
132        })?;
133
134        Ok(proto::OpenBufferResponse {
135            buffer_id: buffer_id.to_proto(),
136        })
137    }
138
139    pub async fn handle_find_search_candidates(
140        this: Model<Self>,
141        envelope: TypedEnvelope<proto::FindSearchCandidates>,
142        mut cx: AsyncAppContext,
143    ) -> Result<proto::FindSearchCandidatesResponse> {
144        let message = envelope.payload;
145        let query = SearchQuery::from_proto(
146            message
147                .query
148                .ok_or_else(|| anyhow!("missing query field"))?,
149        )?;
150        let mut results = this.update(&mut cx, |this, cx| {
151            this.buffer_store.update(cx, |buffer_store, cx| {
152                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
153            })
154        })?;
155
156        let mut response = proto::FindSearchCandidatesResponse {
157            buffer_ids: Vec::new(),
158        };
159
160        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
161
162        while let Some(buffer) = results.next().await {
163            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
164            response.buffer_ids.push(buffer_id.to_proto());
165            buffer_store
166                .update(&mut cx, |buffer_store, cx| {
167                    buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
168                })?
169                .await?;
170        }
171
172        Ok(response)
173    }
174
175    pub async fn handle_list_remote_directory(
176        this: Model<Self>,
177        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
178        cx: AsyncAppContext,
179    ) -> Result<proto::ListRemoteDirectoryResponse> {
180        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
181        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
182
183        let mut entries = Vec::new();
184        let mut response = fs.read_dir(Path::new(&expanded)).await?;
185        while let Some(path) = response.next().await {
186            if let Some(file_name) = path?.file_name() {
187                entries.push(file_name.to_string_lossy().to_string());
188            }
189        }
190        Ok(proto::ListRemoteDirectoryResponse { entries })
191    }
192}