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