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