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 node_runtime::NodeRuntime;
  6use project::{
  7    buffer_store::{BufferStore, BufferStoreEvent},
  8    project_settings::SettingsObserver,
  9    search::SearchQuery,
 10    worktree_store::WorktreeStore,
 11    LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
 12};
 13use remote::ssh_session::ChannelClient;
 14use rpc::{
 15    proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
 16    AnyProtoClient, TypedEnvelope,
 17};
 18use smol::stream::StreamExt;
 19use std::{
 20    path::{Path, PathBuf},
 21    sync::{atomic::AtomicUsize, Arc},
 22};
 23use util::ResultExt;
 24use worktree::Worktree;
 25
 26pub struct HeadlessProject {
 27    pub fs: Arc<dyn Fs>,
 28    pub session: AnyProtoClient,
 29    pub worktree_store: Model<WorktreeStore>,
 30    pub buffer_store: Model<BufferStore>,
 31    pub lsp_store: Model<LspStore>,
 32    pub settings_observer: Model<SettingsObserver>,
 33    pub next_entry_id: Arc<AtomicUsize>,
 34    pub languages: Arc<LanguageRegistry>,
 35}
 36
 37impl HeadlessProject {
 38    pub fn init(cx: &mut AppContext) {
 39        settings::init(cx);
 40        language::init(cx);
 41        project::Project::init_settings(cx);
 42    }
 43
 44    pub fn new(session: Arc<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
 45        let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
 46
 47        let node_runtime = NodeRuntime::unavailable();
 48
 49        languages::init(languages.clone(), node_runtime.clone(), cx);
 50
 51        let worktree_store = cx.new_model(|cx| {
 52            let mut store = WorktreeStore::local(true, fs.clone());
 53            store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 54            store
 55        });
 56        let buffer_store = cx.new_model(|cx| {
 57            let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
 58            buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 59            buffer_store
 60        });
 61        let prettier_store = cx.new_model(|cx| {
 62            PrettierStore::new(
 63                node_runtime,
 64                fs.clone(),
 65                languages.clone(),
 66                worktree_store.clone(),
 67                cx,
 68            )
 69        });
 70
 71        let settings_observer = cx.new_model(|cx| {
 72            let mut observer = SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx);
 73            observer.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 74            observer
 75        });
 76        let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
 77        let lsp_store = cx.new_model(|cx| {
 78            let mut lsp_store = LspStore::new_local(
 79                buffer_store.clone(),
 80                worktree_store.clone(),
 81                prettier_store.clone(),
 82                environment,
 83                languages.clone(),
 84                None,
 85                fs.clone(),
 86                cx,
 87            );
 88            lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
 89            lsp_store
 90        });
 91
 92        cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 93
 94        cx.subscribe(
 95            &buffer_store,
 96            |_this, _buffer_store, event, cx| match event {
 97                BufferStoreEvent::BufferAdded(buffer) => {
 98                    cx.subscribe(buffer, Self::on_buffer_event).detach();
 99                }
100                _ => {}
101            },
102        )
103        .detach();
104
105        let client: AnyProtoClient = session.clone().into();
106
107        session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
108        session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
109        session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
110        session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store);
111        session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
112
113        client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
114        client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists);
115        client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server);
116
117        client.add_model_request_handler(Self::handle_add_worktree);
118        client.add_model_request_handler(Self::handle_open_buffer_by_path);
119        client.add_model_request_handler(Self::handle_find_search_candidates);
120
121        client.add_model_request_handler(BufferStore::handle_update_buffer);
122        client.add_model_message_handler(BufferStore::handle_close_buffer);
123
124        BufferStore::init(&client);
125        WorktreeStore::init(&client);
126        SettingsObserver::init(&client);
127        LspStore::init(&client);
128
129        HeadlessProject {
130            session: client,
131            settings_observer,
132            fs,
133            worktree_store,
134            buffer_store,
135            lsp_store,
136            next_entry_id: Default::default(),
137            languages,
138        }
139    }
140
141    fn on_buffer_event(
142        &mut self,
143        buffer: Model<Buffer>,
144        event: &BufferEvent,
145        cx: &mut ModelContext<Self>,
146    ) {
147        match event {
148            BufferEvent::Operation {
149                operation,
150                is_local: true,
151            } => cx
152                .background_executor()
153                .spawn(self.session.request(proto::UpdateBuffer {
154                    project_id: SSH_PROJECT_ID,
155                    buffer_id: buffer.read(cx).remote_id().to_proto(),
156                    operations: vec![serialize_operation(operation)],
157                }))
158                .detach(),
159            _ => {}
160        }
161    }
162
163    fn on_lsp_store_event(
164        &mut self,
165        _lsp_store: Model<LspStore>,
166        event: &LspStoreEvent,
167        _cx: &mut ModelContext<Self>,
168    ) {
169        match event {
170            LspStoreEvent::LanguageServerUpdate {
171                language_server_id,
172                message,
173            } => {
174                self.session
175                    .send(proto::UpdateLanguageServer {
176                        project_id: SSH_PROJECT_ID,
177                        language_server_id: language_server_id.to_proto(),
178                        variant: Some(message.clone()),
179                    })
180                    .log_err();
181            }
182            _ => {}
183        }
184    }
185
186    pub async fn handle_add_worktree(
187        this: Model<Self>,
188        message: TypedEnvelope<proto::AddWorktree>,
189        mut cx: AsyncAppContext,
190    ) -> Result<proto::AddWorktreeResponse> {
191        use client::ErrorCodeExt;
192        let path = shellexpand::tilde(&message.payload.path).to_string();
193
194        let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?;
195        let path = PathBuf::from(path);
196
197        let canonicalized = match fs.canonicalize(&path).await {
198            Ok(path) => path,
199            Err(e) => {
200                let mut parent = path
201                    .parent()
202                    .ok_or(e)
203                    .map_err(|_| anyhow!("{:?} does not exist", path))?;
204                if parent == Path::new("") {
205                    parent = util::paths::home_dir();
206                }
207                let parent = fs.canonicalize(parent).await.map_err(|_| {
208                    anyhow!(proto::ErrorCode::DevServerProjectPathDoesNotExist
209                        .with_tag("path", &path.to_string_lossy().as_ref()))
210                })?;
211                parent.join(path.file_name().unwrap())
212            }
213        };
214
215        let worktree = this
216            .update(&mut cx.clone(), |this, _| {
217                Worktree::local(
218                    Arc::from(canonicalized),
219                    true,
220                    this.fs.clone(),
221                    this.next_entry_id.clone(),
222                    &mut cx,
223                )
224            })?
225            .await?;
226
227        this.update(&mut cx, |this, cx| {
228            this.worktree_store.update(cx, |worktree_store, cx| {
229                worktree_store.add(&worktree, cx);
230            });
231            worktree.update(cx, |worktree, _| proto::AddWorktreeResponse {
232                worktree_id: worktree.id().to_proto(),
233            })
234        })
235    }
236
237    pub async fn handle_open_buffer_by_path(
238        this: Model<Self>,
239        message: TypedEnvelope<proto::OpenBufferByPath>,
240        mut cx: AsyncAppContext,
241    ) -> Result<proto::OpenBufferResponse> {
242        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
243        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
244            let buffer_store = this.buffer_store.clone();
245            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
246                buffer_store.open_buffer(
247                    ProjectPath {
248                        worktree_id,
249                        path: PathBuf::from(message.payload.path).into(),
250                    },
251                    cx,
252                )
253            });
254            anyhow::Ok((buffer_store, buffer))
255        })??;
256
257        let buffer = buffer.await?;
258        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
259        buffer_store.update(&mut cx, |buffer_store, cx| {
260            buffer_store
261                .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
262                .detach_and_log_err(cx);
263        })?;
264
265        Ok(proto::OpenBufferResponse {
266            buffer_id: buffer_id.to_proto(),
267        })
268    }
269
270    pub async fn handle_find_search_candidates(
271        this: Model<Self>,
272        envelope: TypedEnvelope<proto::FindSearchCandidates>,
273        mut cx: AsyncAppContext,
274    ) -> Result<proto::FindSearchCandidatesResponse> {
275        let message = envelope.payload;
276        let query = SearchQuery::from_proto(
277            message
278                .query
279                .ok_or_else(|| anyhow!("missing query field"))?,
280        )?;
281        let mut results = this.update(&mut cx, |this, cx| {
282            this.buffer_store.update(cx, |buffer_store, cx| {
283                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
284            })
285        })?;
286
287        let mut response = proto::FindSearchCandidatesResponse {
288            buffer_ids: Vec::new(),
289        };
290
291        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
292
293        while let Some(buffer) = results.next().await {
294            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
295            response.buffer_ids.push(buffer_id.to_proto());
296            buffer_store
297                .update(&mut cx, |buffer_store, cx| {
298                    buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
299                })?
300                .await?;
301        }
302
303        Ok(response)
304    }
305
306    pub async fn handle_list_remote_directory(
307        this: Model<Self>,
308        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
309        cx: AsyncAppContext,
310    ) -> Result<proto::ListRemoteDirectoryResponse> {
311        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
312        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
313
314        let mut entries = Vec::new();
315        let mut response = fs.read_dir(Path::new(&expanded)).await?;
316        while let Some(path) = response.next().await {
317            if let Some(file_name) = path?.file_name() {
318                entries.push(file_name.to_string_lossy().to_string());
319            }
320        }
321        Ok(proto::ListRemoteDirectoryResponse { entries })
322    }
323
324    pub async fn handle_check_file_exists(
325        this: Model<Self>,
326        envelope: TypedEnvelope<proto::CheckFileExists>,
327        cx: AsyncAppContext,
328    ) -> Result<proto::CheckFileExistsResponse> {
329        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
330        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
331
332        let exists = fs.is_file(&PathBuf::from(expanded.clone())).await;
333
334        Ok(proto::CheckFileExistsResponse {
335            exists,
336            path: expanded,
337        })
338    }
339
340    pub async fn handle_shutdown_remote_server(
341        _this: Model<Self>,
342        _envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
343        cx: AsyncAppContext,
344    ) -> Result<proto::Ack> {
345        cx.spawn(|cx| async move {
346            cx.update(|cx| {
347                // TODO: This is a hack, because in a headless project, shutdown isn't executed
348                // when calling quit, but it should be.
349                cx.shutdown();
350                cx.quit();
351            })
352        })
353        .detach();
354
355        Ok(proto::Ack {})
356    }
357}