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}