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