headless_project.rs

  1use anyhow::Result;
  2use fs::Fs;
  3use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
  4use project::{
  5    buffer_store::{BufferStore, BufferStoreEvent},
  6    worktree_store::WorktreeStore,
  7    ProjectPath, WorktreeId, WorktreeSettings,
  8};
  9use remote::SshSession;
 10use rpc::{
 11    proto::{self, AnyProtoClient, PeerId},
 12    TypedEnvelope,
 13};
 14use settings::{Settings as _, SettingsStore};
 15use smol::stream::StreamExt;
 16use std::{
 17    path::{Path, PathBuf},
 18    sync::{atomic::AtomicUsize, Arc},
 19};
 20use util::ResultExt as _;
 21use worktree::Worktree;
 22
 23const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
 24const PROJECT_ID: u64 = 0;
 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 next_entry_id: Arc<AtomicUsize>,
 32}
 33
 34impl HeadlessProject {
 35    pub fn init(cx: &mut AppContext) {
 36        cx.set_global(SettingsStore::new(cx));
 37        WorktreeSettings::register(cx);
 38    }
 39
 40    pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
 41        let this = cx.weak_model();
 42
 43        let worktree_store = cx.new_model(|_| WorktreeStore::new(true));
 44        let buffer_store =
 45            cx.new_model(|cx| BufferStore::new(worktree_store.clone(), Some(PROJECT_ID), cx));
 46        cx.subscribe(&buffer_store, Self::on_buffer_store_event)
 47            .detach();
 48
 49        session.add_request_handler(this.clone(), Self::handle_list_remote_directory);
 50        session.add_request_handler(this.clone(), Self::handle_add_worktree);
 51        session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
 52
 53        session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_blame_buffer);
 54        session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_update_buffer);
 55        session.add_request_handler(buffer_store.downgrade(), BufferStore::handle_save_buffer);
 56
 57        session.add_request_handler(
 58            worktree_store.downgrade(),
 59            WorktreeStore::handle_create_project_entry,
 60        );
 61        session.add_request_handler(
 62            worktree_store.downgrade(),
 63            WorktreeStore::handle_rename_project_entry,
 64        );
 65        session.add_request_handler(
 66            worktree_store.downgrade(),
 67            WorktreeStore::handle_copy_project_entry,
 68        );
 69        session.add_request_handler(
 70            worktree_store.downgrade(),
 71            WorktreeStore::handle_delete_project_entry,
 72        );
 73        session.add_request_handler(
 74            worktree_store.downgrade(),
 75            WorktreeStore::handle_expand_project_entry,
 76        );
 77
 78        HeadlessProject {
 79            session: session.into(),
 80            fs,
 81            worktree_store,
 82            buffer_store,
 83            next_entry_id: Default::default(),
 84        }
 85    }
 86
 87    pub async fn handle_add_worktree(
 88        this: Model<Self>,
 89        message: TypedEnvelope<proto::AddWorktree>,
 90        mut cx: AsyncAppContext,
 91    ) -> Result<proto::AddWorktreeResponse> {
 92        let path = shellexpand::tilde(&message.payload.path).to_string();
 93        let worktree = this
 94            .update(&mut cx.clone(), |this, _| {
 95                Worktree::local(
 96                    Path::new(&path),
 97                    true,
 98                    this.fs.clone(),
 99                    this.next_entry_id.clone(),
100                    &mut cx,
101                )
102            })?
103            .await?;
104
105        this.update(&mut cx, |this, cx| {
106            let session = this.session.clone();
107            this.worktree_store.update(cx, |worktree_store, cx| {
108                worktree_store.add(&worktree, cx);
109            });
110            worktree.update(cx, |worktree, cx| {
111                worktree.observe_updates(0, cx, move |update| {
112                    session.send(update).ok();
113                    futures::future::ready(true)
114                });
115                proto::AddWorktreeResponse {
116                    worktree_id: worktree.id().to_proto(),
117                }
118            })
119        })
120    }
121
122    pub async fn handle_open_buffer_by_path(
123        this: Model<Self>,
124        message: TypedEnvelope<proto::OpenBufferByPath>,
125        mut cx: AsyncAppContext,
126    ) -> Result<proto::OpenBufferResponse> {
127        let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
128        let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
129            let buffer_store = this.buffer_store.clone();
130            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
131                buffer_store.open_buffer(
132                    ProjectPath {
133                        worktree_id,
134                        path: PathBuf::from(message.payload.path).into(),
135                    },
136                    cx,
137                )
138            });
139            anyhow::Ok((buffer_store, buffer, this.session.clone()))
140        })??;
141
142        let buffer = buffer.await?;
143        let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
144
145        cx.spawn(|mut cx| async move {
146            BufferStore::create_buffer_for_peer(
147                buffer_store,
148                PEER_ID,
149                buffer_id,
150                PROJECT_ID,
151                session,
152                &mut cx,
153            )
154            .await
155        })
156        .detach();
157
158        Ok(proto::OpenBufferResponse {
159            buffer_id: buffer_id.to_proto(),
160        })
161    }
162
163    pub async fn handle_list_remote_directory(
164        this: Model<Self>,
165        envelope: TypedEnvelope<proto::ListRemoteDirectory>,
166        cx: AsyncAppContext,
167    ) -> Result<proto::ListRemoteDirectoryResponse> {
168        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
169        let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
170
171        let mut entries = Vec::new();
172        let mut response = fs.read_dir(Path::new(&expanded)).await?;
173        while let Some(path) = response.next().await {
174            if let Some(file_name) = path?.file_name() {
175                entries.push(file_name.to_string_lossy().to_string());
176            }
177        }
178        Ok(proto::ListRemoteDirectoryResponse { entries })
179    }
180
181    pub fn on_buffer_store_event(
182        &mut self,
183        _: Model<BufferStore>,
184        event: &BufferStoreEvent,
185        _: &mut ModelContext<Self>,
186    ) {
187        match event {
188            BufferStoreEvent::MessageToReplicas(message) => {
189                self.session
190                    .send_dynamic(message.as_ref().clone())
191                    .log_err();
192            }
193            _ => {}
194        }
195    }
196}