headless_project.rs

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