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
116        client.add_model_request_handler(Self::handle_add_worktree);
117        client.add_model_request_handler(Self::handle_open_buffer_by_path);
118        client.add_model_request_handler(Self::handle_find_search_candidates);
119
120        client.add_model_request_handler(BufferStore::handle_update_buffer);
121        client.add_model_message_handler(BufferStore::handle_close_buffer);
122
123        BufferStore::init(&client);
124        WorktreeStore::init(&client);
125        SettingsObserver::init(&client);
126        LspStore::init(&client);
127
128        HeadlessProject {
129            session: client,
130            settings_observer,
131            fs,
132            worktree_store,
133            buffer_store,
134            lsp_store,
135            next_entry_id: Default::default(),
136            languages,
137        }
138    }
139
140    fn on_buffer_event(
141        &mut self,
142        buffer: Model<Buffer>,
143        event: &BufferEvent,
144        cx: &mut ModelContext<Self>,
145    ) {
146        match event {
147            BufferEvent::Operation {
148                operation,
149                is_local: true,
150            } => cx
151                .background_executor()
152                .spawn(self.session.request(proto::UpdateBuffer {
153                    project_id: SSH_PROJECT_ID,
154                    buffer_id: buffer.read(cx).remote_id().to_proto(),
155                    operations: vec![serialize_operation(operation)],
156                }))
157                .detach(),
158            _ => {}
159        }
160    }
161
162    fn on_lsp_store_event(
163        &mut self,
164        _lsp_store: Model<LspStore>,
165        event: &LspStoreEvent,
166        _cx: &mut ModelContext<Self>,
167    ) {
168        match event {
169            LspStoreEvent::LanguageServerUpdate {
170                language_server_id,
171                message,
172            } => {
173                self.session
174                    .send(proto::UpdateLanguageServer {
175                        project_id: SSH_PROJECT_ID,
176                        language_server_id: language_server_id.to_proto(),
177                        variant: Some(message.clone()),
178                    })
179                    .log_err();
180            }
181            _ => {}
182        }
183    }
184
185    pub async fn handle_add_worktree(
186        this: Model<Self>,
187        message: TypedEnvelope<proto::AddWorktree>,
188        mut cx: AsyncAppContext,
189    ) -> Result<proto::AddWorktreeResponse> {
190        use client::ErrorCodeExt;
191        let path = shellexpand::tilde(&message.payload.path).to_string();
192
193        let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?;
194        let path = PathBuf::from(path);
195
196        let canonicalized = match fs.canonicalize(&path).await {
197            Ok(path) => path,
198            Err(e) => {
199                let mut parent = path
200                    .parent()
201                    .ok_or(e)
202                    .map_err(|_| anyhow!("{:?} does not exist", path))?;
203                if parent == Path::new("") {
204                    parent = util::paths::home_dir();
205                }
206                let parent = fs.canonicalize(parent).await.map_err(|_| {
207                    anyhow!(proto::ErrorCode::DevServerProjectPathDoesNotExist
208                        .with_tag("path", &path.to_string_lossy().as_ref()))
209                })?;
210                parent.join(path.file_name().unwrap())
211            }
212        };
213
214        let worktree = this
215            .update(&mut cx.clone(), |this, _| {
216                Worktree::local(
217                    Arc::from(canonicalized),
218                    true,
219                    this.fs.clone(),
220                    this.next_entry_id.clone(),
221                    &mut cx,
222                )
223            })?
224            .await?;
225
226        this.update(&mut cx, |this, cx| {
227            this.worktree_store.update(cx, |worktree_store, cx| {
228                worktree_store.add(&worktree, cx);
229            });
230            worktree.update(cx, |worktree, _| proto::AddWorktreeResponse {
231                worktree_id: worktree.id().to_proto(),
232            })
233        })
234    }
235
236    pub async fn handle_open_buffer_by_path(
237        this: Model<Self>,
238        message: TypedEnvelope<proto::OpenBufferByPath>,
239        mut cx: AsyncAppContext,
240    ) -> Result<proto::OpenBufferResponse> {
241        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
242        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
243            let buffer_store = this.buffer_store.clone();
244            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
245                buffer_store.open_buffer(
246                    ProjectPath {
247                        worktree_id,
248                        path: PathBuf::from(message.payload.path).into(),
249                    },
250                    cx,
251                )
252            });
253            anyhow::Ok((buffer_store, buffer))
254        })??;
255
256        let buffer = buffer.await?;
257        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
258        buffer_store.update(&mut cx, |buffer_store, cx| {
259            buffer_store
260                .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
261                .detach_and_log_err(cx);
262        })?;
263
264        Ok(proto::OpenBufferResponse {
265            buffer_id: buffer_id.to_proto(),
266        })
267    }
268
269    pub async fn handle_find_search_candidates(
270        this: Model<Self>,
271        envelope: TypedEnvelope<proto::FindSearchCandidates>,
272        mut cx: AsyncAppContext,
273    ) -> Result<proto::FindSearchCandidatesResponse> {
274        let message = envelope.payload;
275        let query = SearchQuery::from_proto(
276            message
277                .query
278                .ok_or_else(|| anyhow!("missing query field"))?,
279        )?;
280        let mut results = this.update(&mut cx, |this, cx| {
281            this.buffer_store.update(cx, |buffer_store, cx| {
282                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
283            })
284        })?;
285
286        let mut response = proto::FindSearchCandidatesResponse {
287            buffer_ids: Vec::new(),
288        };
289
290        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
291
292        while let Some(buffer) = results.next().await {
293            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
294            response.buffer_ids.push(buffer_id.to_proto());
295            buffer_store
296                .update(&mut cx, |buffer_store, cx| {
297                    buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
298                })?
299                .await?;
300        }
301
302        Ok(response)
303    }
304
305    pub async fn handle_list_remote_directory(
306        this: Model<Self>,
307        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
308        cx: AsyncAppContext,
309    ) -> Result<proto::ListRemoteDirectoryResponse> {
310        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
311        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
312
313        let mut entries = Vec::new();
314        let mut response = fs.read_dir(Path::new(&expanded)).await?;
315        while let Some(path) = response.next().await {
316            if let Some(file_name) = path?.file_name() {
317                entries.push(file_name.to_string_lossy().to_string());
318            }
319        }
320        Ok(proto::ListRemoteDirectoryResponse { entries })
321    }
322
323    pub async fn handle_check_file_exists(
324        this: Model<Self>,
325        envelope: TypedEnvelope<proto::CheckFileExists>,
326        cx: AsyncAppContext,
327    ) -> Result<proto::CheckFileExistsResponse> {
328        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
329        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
330
331        let exists = fs.is_file(&PathBuf::from(expanded.clone())).await;
332
333        Ok(proto::CheckFileExistsResponse {
334            exists,
335            path: expanded,
336        })
337    }
338}