headless_project.rs

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