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 let mut lsp_store = LspStore::new_local(
61 buffer_store.clone(),
62 worktree_store.clone(),
63 environment,
64 languages,
65 None,
66 fs.clone(),
67 cx,
68 );
69 lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
70 lsp_store
71 });
72
73 let client: AnyProtoClient = session.clone().into();
74
75 session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store);
76 session.subscribe_to_entity(SSH_PROJECT_ID, &buffer_store);
77 session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
78 session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store);
79 session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
80
81 client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
82
83 client.add_model_request_handler(Self::handle_add_worktree);
84 client.add_model_request_handler(Self::handle_open_buffer_by_path);
85 client.add_model_request_handler(Self::handle_find_search_candidates);
86
87 client.add_model_request_handler(BufferStore::handle_update_buffer);
88 client.add_model_message_handler(BufferStore::handle_close_buffer);
89
90 BufferStore::init(&client);
91 WorktreeStore::init(&client);
92 SettingsObserver::init(&client);
93
94 HeadlessProject {
95 session: client,
96 settings_observer,
97 fs,
98 worktree_store,
99 buffer_store,
100 lsp_store,
101 next_entry_id: Default::default(),
102 }
103 }
104
105 pub async fn handle_add_worktree(
106 this: Model<Self>,
107 message: TypedEnvelope<proto::AddWorktree>,
108 mut cx: AsyncAppContext,
109 ) -> Result<proto::AddWorktreeResponse> {
110 let path = shellexpand::tilde(&message.payload.path).to_string();
111 let worktree = this
112 .update(&mut cx.clone(), |this, _| {
113 Worktree::local(
114 Path::new(&path),
115 true,
116 this.fs.clone(),
117 this.next_entry_id.clone(),
118 &mut cx,
119 )
120 })?
121 .await?;
122
123 this.update(&mut cx, |this, cx| {
124 let session = this.session.clone();
125 this.worktree_store.update(cx, |worktree_store, cx| {
126 worktree_store.add(&worktree, cx);
127 });
128 worktree.update(cx, |worktree, cx| {
129 worktree.observe_updates(0, cx, move |update| {
130 session.send(update).ok();
131 futures::future::ready(true)
132 });
133 proto::AddWorktreeResponse {
134 worktree_id: worktree.id().to_proto(),
135 }
136 })
137 })
138 }
139
140 pub async fn handle_open_buffer_by_path(
141 this: Model<Self>,
142 message: TypedEnvelope<proto::OpenBufferByPath>,
143 mut cx: AsyncAppContext,
144 ) -> Result<proto::OpenBufferResponse> {
145 let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
146 let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
147 let buffer_store = this.buffer_store.clone();
148 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
149 buffer_store.open_buffer(
150 ProjectPath {
151 worktree_id,
152 path: PathBuf::from(message.payload.path).into(),
153 },
154 cx,
155 )
156 });
157 anyhow::Ok((buffer_store, buffer))
158 })??;
159
160 let buffer = buffer.await?;
161 let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
162 buffer_store.update(&mut cx, |buffer_store, cx| {
163 buffer_store
164 .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
165 .detach_and_log_err(cx);
166 })?;
167
168 Ok(proto::OpenBufferResponse {
169 buffer_id: buffer_id.to_proto(),
170 })
171 }
172
173 pub async fn handle_find_search_candidates(
174 this: Model<Self>,
175 envelope: TypedEnvelope<proto::FindSearchCandidates>,
176 mut cx: AsyncAppContext,
177 ) -> Result<proto::FindSearchCandidatesResponse> {
178 let message = envelope.payload;
179 let query = SearchQuery::from_proto(
180 message
181 .query
182 .ok_or_else(|| anyhow!("missing query field"))?,
183 )?;
184 let mut results = this.update(&mut cx, |this, cx| {
185 this.buffer_store.update(cx, |buffer_store, cx| {
186 buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
187 })
188 })?;
189
190 let mut response = proto::FindSearchCandidatesResponse {
191 buffer_ids: Vec::new(),
192 };
193
194 let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
195
196 while let Some(buffer) = results.next().await {
197 let buffer_id = buffer.update(&mut cx, |this, _| this.remote_id())?;
198 response.buffer_ids.push(buffer_id.to_proto());
199 buffer_store
200 .update(&mut cx, |buffer_store, cx| {
201 buffer_store.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
202 })?
203 .await?;
204 }
205
206 Ok(response)
207 }
208
209 pub async fn handle_list_remote_directory(
210 this: Model<Self>,
211 envelope: TypedEnvelope<proto::ListRemoteDirectory>,
212 cx: AsyncAppContext,
213 ) -> Result<proto::ListRemoteDirectoryResponse> {
214 let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
215 let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
216
217 let mut entries = Vec::new();
218 let mut response = fs.read_dir(Path::new(&expanded)).await?;
219 while let Some(path) = response.next().await {
220 if let Some(file_name) = path?.file_name() {
221 entries.push(file_name.to_string_lossy().to_string());
222 }
223 }
224 Ok(proto::ListRemoteDirectoryResponse { entries })
225 }
226}