headless_project.rs

  1use anyhow::{anyhow, Result};
  2use fs::Fs;
  3use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
  4use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
  5use project::{
  6    buffer_store::{BufferStore, BufferStoreEvent},
  7    project_settings::SettingsObserver,
  8    search::SearchQuery,
  9    worktree_store::WorktreeStore,
 10    LspStore, ProjectPath, WorktreeId,
 11};
 12use remote::SshSession;
 13use rpc::{
 14    proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
 15    AnyProtoClient, TypedEnvelope,
 16};
 17use smol::stream::StreamExt;
 18use std::{
 19    path::{Path, PathBuf},
 20    sync::{atomic::AtomicUsize, Arc},
 21};
 22use worktree::Worktree;
 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 lsp_store: Model<LspStore>,
 30    pub settings_observer: Model<SettingsObserver>,
 31    pub next_entry_id: Arc<AtomicUsize>,
 32    pub languages: Arc<LanguageRegistry>,
 33}
 34
 35impl HeadlessProject {
 36    pub fn init(cx: &mut AppContext) {
 37        settings::init(cx);
 38        language::init(cx);
 39        project::Project::init_settings(cx);
 40    }
 41
 42    pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
 43        let mut languages = LanguageRegistry::new(cx.background_executor().clone());
 44        languages
 45            .set_language_server_download_dir(PathBuf::from("/Users/conrad/what-could-go-wrong"));
 46
 47        let languages = Arc::new(languages);
 48
 49        let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone()));
 50        let buffer_store = cx.new_model(|cx| {
 51            let mut buffer_store =
 52                BufferStore::new(worktree_store.clone(), Some(SSH_PROJECT_ID), cx);
 53            buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 54            buffer_store
 55        });
 56        let settings_observer = cx.new_model(|cx| {
 57            let mut observer = SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx);
 58            observer.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 59            observer
 60        });
 61        let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
 62        let lsp_store = cx.new_model(|cx| {
 63            let mut lsp_store = LspStore::new_local(
 64                buffer_store.clone(),
 65                worktree_store.clone(),
 66                environment,
 67                languages.clone(),
 68                None,
 69                fs.clone(),
 70                cx,
 71            );
 72            lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 73            lsp_store
 74        });
 75
 76        cx.subscribe(
 77            &buffer_store,
 78            |_this, _buffer_store, event, cx| match event {
 79                BufferStoreEvent::BufferAdded(buffer) => {
 80                    cx.subscribe(buffer, Self::on_buffer_event).detach();
 81                }
 82                _ => {}
 83            },
 84        )
 85        .detach();
 86
 87        let client: AnyProtoClient = session.clone().into();
 88
 89        session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
 90        session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
 91        session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
 92        session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store);
 93        session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
 94
 95        client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
 96
 97        client.add_model_request_handler(Self::handle_add_worktree);
 98        client.add_model_request_handler(Self::handle_open_buffer_by_path);
 99        client.add_model_request_handler(Self::handle_find_search_candidates);
100
101        client.add_model_request_handler(BufferStore::handle_update_buffer);
102        client.add_model_message_handler(BufferStore::handle_close_buffer);
103
104        client.add_model_request_handler(LspStore::handle_create_language_server);
105        client.add_model_request_handler(LspStore::handle_which_command);
106        client.add_model_request_handler(LspStore::handle_shell_env);
107
108        BufferStore::init(&client);
109        WorktreeStore::init(&client);
110        SettingsObserver::init(&client);
111        LspStore::init(&client);
112
113        HeadlessProject {
114            session: client,
115            settings_observer,
116            fs,
117            worktree_store,
118            buffer_store,
119            lsp_store,
120            next_entry_id: Default::default(),
121            languages,
122        }
123    }
124
125    fn on_buffer_event(
126        &mut self,
127        buffer: Model<Buffer>,
128        event: &BufferEvent,
129        cx: &mut ModelContext<Self>,
130    ) {
131        match event {
132            BufferEvent::Operation(op) => cx
133                .background_executor()
134                .spawn(self.session.request(proto::UpdateBuffer {
135                    project_id: SSH_PROJECT_ID,
136                    buffer_id: buffer.read(cx).remote_id().to_proto(),
137                    operations: vec![serialize_operation(op)],
138                }))
139                .detach(),
140            _ => {}
141        }
142    }
143
144    pub async fn handle_add_worktree(
145        this: Model<Self>,
146        message: TypedEnvelope<proto::AddWorktree>,
147        mut cx: AsyncAppContext,
148    ) -> Result<proto::AddWorktreeResponse> {
149        let path = shellexpand::tilde(&message.payload.path).to_string();
150        let worktree = this
151            .update(&mut cx.clone(), |this, _| {
152                Worktree::local(
153                    Path::new(&path),
154                    true,
155                    this.fs.clone(),
156                    this.next_entry_id.clone(),
157                    &mut cx,
158                )
159            })?
160            .await?;
161
162        this.update(&mut cx, |this, cx| {
163            let session = this.session.clone();
164            this.worktree_store.update(cx, |worktree_store, cx| {
165                worktree_store.add(&worktree, cx);
166            });
167            worktree.update(cx, |worktree, cx| {
168                worktree.observe_updates(0, cx, move |update| {
169                    session.send(update).ok();
170                    futures::future::ready(true)
171                });
172                proto::AddWorktreeResponse {
173                    worktree_id: worktree.id().to_proto(),
174                }
175            })
176        })
177    }
178
179    pub async fn handle_open_buffer_by_path(
180        this: Model<Self>,
181        message: TypedEnvelope<proto::OpenBufferByPath>,
182        mut cx: AsyncAppContext,
183    ) -> Result<proto::OpenBufferResponse> {
184        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
185        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
186            let buffer_store = this.buffer_store.clone();
187            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
188                buffer_store.open_buffer(
189                    ProjectPath {
190                        worktree_id,
191                        path: PathBuf::from(message.payload.path).into(),
192                    },
193                    cx,
194                )
195            });
196            anyhow::Ok((buffer_store, buffer))
197        })??;
198
199        let buffer = buffer.await?;
200        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
201        buffer_store.update(&mut cx, |buffer_store, cx| {
202            buffer_store
203                .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
204                .detach_and_log_err(cx);
205        })?;
206
207        Ok(proto::OpenBufferResponse {
208            buffer_id: buffer_id.to_proto(),
209        })
210    }
211
212    pub async fn handle_find_search_candidates(
213        this: Model<Self>,
214        envelope: TypedEnvelope<proto::FindSearchCandidates>,
215        mut cx: AsyncAppContext,
216    ) -> Result<proto::FindSearchCandidatesResponse> {
217        let message = envelope.payload;
218        let query = SearchQuery::from_proto(
219            message
220                .query
221                .ok_or_else(|| anyhow!("missing query field"))?,
222        )?;
223        let mut results = this.update(&mut cx, |this, cx| {
224            this.buffer_store.update(cx, |buffer_store, cx| {
225                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
226            })
227        })?;
228
229        let mut response = proto::FindSearchCandidatesResponse {
230            buffer_ids: Vec::new(),
231        };
232
233        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
234
235        while let Some(buffer) = results.next().await {
236            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
237            response.buffer_ids.push(buffer_id.to_proto());
238            buffer_store
239                .update(&mut cx, |buffer_store, cx| {
240                    buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
241                })?
242                .await?;
243        }
244
245        Ok(response)
246    }
247
248    pub async fn handle_list_remote_directory(
249        this: Model<Self>,
250        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
251        cx: AsyncAppContext,
252    ) -> Result<proto::ListRemoteDirectoryResponse> {
253        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
254        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
255
256        let mut entries = Vec::new();
257        let mut response = fs.read_dir(Path::new(&expanded)).await?;
258        while let Some(path) = response.next().await {
259            if let Some(file_name) = path?.file_name() {
260                entries.push(file_name.to_string_lossy().to_string());
261            }
262        }
263        Ok(proto::ListRemoteDirectoryResponse { entries })
264    }
265}