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