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            LspStore::new(
 61                buffer_store.clone(),
 62                worktree_store.clone(),
 63                Some(environment),
 64                languages,
 65                None,
 66                fs.clone(),
 67                Some(session.clone().into()),
 68                None,
 69                Some(0),
 70                cx,
 71            )
 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        BufferStore::init(&client);
 92        WorktreeStore::init(&client);
 93        SettingsObserver::init(&client);
 94
 95        HeadlessProject {
 96            session: client,
 97            settings_observer,
 98            fs,
 99            worktree_store,
100            buffer_store,
101            lsp_store,
102            next_entry_id: Default::default(),
103        }
104    }
105
106    pub async fn handle_add_worktree(
107        this: Model<Self>,
108        message: TypedEnvelope<proto::AddWorktree>,
109        mut cx: AsyncAppContext,
110    ) -> Result<proto::AddWorktreeResponse> {
111        let path = shellexpand::tilde(&message.payload.path).to_string();
112        let worktree = this
113            .update(&mut cx.clone(), |this, _| {
114                Worktree::local(
115                    Path::new(&path),
116                    true,
117                    this.fs.clone(),
118                    this.next_entry_id.clone(),
119                    &mut cx,
120                )
121            })?
122            .await?;
123
124        this.update(&mut cx, |this, cx| {
125            let session = this.session.clone();
126            this.worktree_store.update(cx, |worktree_store, cx| {
127                worktree_store.add(&worktree, cx);
128            });
129            worktree.update(cx, |worktree, cx| {
130                worktree.observe_updates(0, cx, move |update| {
131                    session.send(update).ok();
132                    futures::future::ready(true)
133                });
134                proto::AddWorktreeResponse {
135                    worktree_id: worktree.id().to_proto(),
136                }
137            })
138        })
139    }
140
141    pub async fn handle_open_buffer_by_path(
142        this: Model<Self>,
143        message: TypedEnvelope<proto::OpenBufferByPath>,
144        mut cx: AsyncAppContext,
145    ) -> Result<proto::OpenBufferResponse> {
146        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
147        let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
148            let buffer_store = this.buffer_store.clone();
149            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
150                buffer_store.open_buffer(
151                    ProjectPath {
152                        worktree_id,
153                        path: PathBuf::from(message.payload.path).into(),
154                    },
155                    cx,
156                )
157            });
158            anyhow::Ok((buffer_store, buffer))
159        })??;
160
161        let buffer = buffer.await?;
162        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
163        buffer_store.update(&mut cx, |buffer_store, cx| {
164            buffer_store
165                .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
166                .detach_and_log_err(cx);
167        })?;
168
169        Ok(proto::OpenBufferResponse {
170            buffer_id: buffer_id.to_proto(),
171        })
172    }
173
174    pub async fn handle_find_search_candidates(
175        this: Model<Self>,
176        envelope: TypedEnvelope<proto::FindSearchCandidates>,
177        mut cx: AsyncAppContext,
178    ) -> Result<proto::FindSearchCandidatesResponse> {
179        let message = envelope.payload;
180        let query = SearchQuery::from_proto(
181            message
182                .query
183                .ok_or_else(|| anyhow!("missing query field"))?,
184        )?;
185        let mut results = this.update(&mut cx, |this, cx| {
186            this.buffer_store.update(cx, |buffer_store, cx| {
187                buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
188            })
189        })?;
190
191        let mut response = proto::FindSearchCandidatesResponse {
192            buffer_ids: Vec::new(),
193        };
194
195        let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
196
197        while let Some(buffer) = results.next().await {
198            let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
199            response.buffer_ids.push(buffer_id.to_proto());
200            buffer_store
201                .update(&mut cx, |buffer_store, cx| {
202                    buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
203                })?
204                .await?;
205        }
206
207        Ok(response)
208    }
209
210    pub async fn handle_list_remote_directory(
211        this: Model<Self>,
212        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
213        cx: AsyncAppContext,
214    ) -> Result<proto::ListRemoteDirectoryResponse> {
215        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
216        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
217
218        let mut entries = Vec::new();
219        let mut response = fs.read_dir(Path::new(&expanded)).await?;
220        while let Some(path) = response.next().await {
221            if let Some(file_name) = path?.file_name() {
222                entries.push(file_name.to_string_lossy().to_string());
223            }
224        }
225        Ok(proto::ListRemoteDirectoryResponse { entries })
226    }
227}