1use anyhow::{Context as _, Result, anyhow};
2use language::File;
3use lsp::LanguageServerId;
4
5use extension::ExtensionHostProxy;
6use extension_host::headless_host::HeadlessExtensionStore;
7use fs::Fs;
8use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel};
9use http_client::HttpClient;
10use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation};
11use node_runtime::NodeRuntime;
12use project::{
13 LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, ProjectPath,
14 ToolchainStore, WorktreeId,
15 agent_server_store::AgentServerStore,
16 buffer_store::{BufferStore, BufferStoreEvent},
17 debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore},
18 git_store::GitStore,
19 image_store::ImageId,
20 lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind, LogKind},
21 project_settings::SettingsObserver,
22 search::SearchQuery,
23 task_store::TaskStore,
24 worktree_store::WorktreeStore,
25};
26use rpc::{
27 AnyProtoClient, TypedEnvelope,
28 proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID},
29};
30
31use settings::initial_server_settings_content;
32use smol::stream::StreamExt;
33use std::{
34 num::NonZeroU64,
35 path::{Path, PathBuf},
36 sync::{
37 Arc,
38 atomic::{AtomicU64, AtomicUsize, Ordering},
39 },
40};
41use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
42use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
43use worktree::Worktree;
44
45pub struct HeadlessProject {
46 pub fs: Arc<dyn Fs>,
47 pub session: AnyProtoClient,
48 pub worktree_store: Entity<WorktreeStore>,
49 pub buffer_store: Entity<BufferStore>,
50 pub lsp_store: Entity<LspStore>,
51 pub task_store: Entity<TaskStore>,
52 pub dap_store: Entity<DapStore>,
53 pub agent_server_store: Entity<AgentServerStore>,
54 pub settings_observer: Entity<SettingsObserver>,
55 pub next_entry_id: Arc<AtomicUsize>,
56 pub languages: Arc<LanguageRegistry>,
57 pub extensions: Entity<HeadlessExtensionStore>,
58 pub git_store: Entity<GitStore>,
59 pub environment: Entity<ProjectEnvironment>,
60 // Used mostly to keep alive the toolchain store for RPC handlers.
61 // Local variant is used within LSP store, but that's a separate entity.
62 pub _toolchain_store: Entity<ToolchainStore>,
63}
64
65pub struct HeadlessAppState {
66 pub session: AnyProtoClient,
67 pub fs: Arc<dyn Fs>,
68 pub http_client: Arc<dyn HttpClient>,
69 pub node_runtime: NodeRuntime,
70 pub languages: Arc<LanguageRegistry>,
71 pub extension_host_proxy: Arc<ExtensionHostProxy>,
72}
73
74impl HeadlessProject {
75 pub fn init(cx: &mut App) {
76 settings::init(cx);
77 log_store::init(true, cx);
78 }
79
80 pub fn new(
81 HeadlessAppState {
82 session,
83 fs,
84 http_client,
85 node_runtime,
86 languages,
87 extension_host_proxy: proxy,
88 }: HeadlessAppState,
89 cx: &mut Context<Self>,
90 ) -> Self {
91 debug_adapter_extension::init(proxy.clone(), cx);
92 languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx);
93
94 let worktree_store = cx.new(|cx| {
95 let mut store = WorktreeStore::local(true, fs.clone());
96 store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
97 store
98 });
99
100 let environment =
101 cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx));
102 let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
103 let toolchain_store = cx.new(|cx| {
104 ToolchainStore::local(
105 languages.clone(),
106 worktree_store.clone(),
107 environment.clone(),
108 manifest_tree.clone(),
109 fs.clone(),
110 cx,
111 )
112 });
113
114 let buffer_store = cx.new(|cx| {
115 let mut buffer_store = BufferStore::local(worktree_store.clone(), cx);
116 buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
117 buffer_store
118 });
119
120 let breakpoint_store =
121 cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone()));
122
123 let dap_store = cx.new(|cx| {
124 let mut dap_store = DapStore::new_local(
125 http_client.clone(),
126 node_runtime.clone(),
127 fs.clone(),
128 environment.clone(),
129 toolchain_store.read(cx).as_language_toolchain_store(),
130 worktree_store.clone(),
131 breakpoint_store.clone(),
132 true,
133 cx,
134 );
135 dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
136 dap_store
137 });
138
139 let git_store = cx.new(|cx| {
140 let mut store = GitStore::local(
141 &worktree_store,
142 buffer_store.clone(),
143 environment.clone(),
144 fs.clone(),
145 cx,
146 );
147 store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
148 store
149 });
150
151 let prettier_store = cx.new(|cx| {
152 PrettierStore::new(
153 node_runtime.clone(),
154 fs.clone(),
155 languages.clone(),
156 worktree_store.clone(),
157 cx,
158 )
159 });
160
161 let task_store = cx.new(|cx| {
162 let mut task_store = TaskStore::local(
163 buffer_store.downgrade(),
164 worktree_store.clone(),
165 toolchain_store.read(cx).as_language_toolchain_store(),
166 environment.clone(),
167 cx,
168 );
169 task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
170 task_store
171 });
172 let settings_observer = cx.new(|cx| {
173 let mut observer = SettingsObserver::new_local(
174 fs.clone(),
175 worktree_store.clone(),
176 task_store.clone(),
177 cx,
178 );
179 observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
180 observer
181 });
182
183 let lsp_store = cx.new(|cx| {
184 let mut lsp_store = LspStore::new_local(
185 buffer_store.clone(),
186 worktree_store.clone(),
187 prettier_store.clone(),
188 toolchain_store
189 .read(cx)
190 .as_local_store()
191 .expect("Toolchain store to be local")
192 .clone(),
193 environment.clone(),
194 manifest_tree,
195 languages.clone(),
196 http_client.clone(),
197 fs.clone(),
198 cx,
199 );
200 lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
201 lsp_store
202 });
203
204 let agent_server_store = cx.new(|cx| {
205 let mut agent_server_store = AgentServerStore::local(
206 node_runtime.clone(),
207 fs.clone(),
208 environment.clone(),
209 http_client.clone(),
210 cx,
211 );
212 agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
213 agent_server_store
214 });
215
216 cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
217 language_extension::init(
218 language_extension::LspAccess::ViaLspStore(lsp_store.clone()),
219 proxy.clone(),
220 languages.clone(),
221 );
222
223 cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| {
224 if let BufferStoreEvent::BufferAdded(buffer) = event {
225 cx.subscribe(buffer, Self::on_buffer_event).detach();
226 }
227 })
228 .detach();
229
230 let extensions = HeadlessExtensionStore::new(
231 fs.clone(),
232 http_client.clone(),
233 paths::remote_extensions_dir().to_path_buf(),
234 proxy,
235 node_runtime,
236 cx,
237 );
238
239 // local_machine -> ssh handlers
240 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store);
241 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store);
242 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity());
243 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store);
244 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store);
245 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store);
246 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store);
247 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer);
248 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store);
249 session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store);
250
251 session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory);
252 session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata);
253 session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server);
254 session.add_request_handler(cx.weak_entity(), Self::handle_ping);
255 session.add_request_handler(cx.weak_entity(), Self::handle_get_processes);
256
257 session.add_entity_request_handler(Self::handle_add_worktree);
258 session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree);
259
260 session.add_entity_request_handler(Self::handle_open_buffer_by_path);
261 session.add_entity_request_handler(Self::handle_open_new_buffer);
262 session.add_entity_request_handler(Self::handle_find_search_candidates);
263 session.add_entity_request_handler(Self::handle_open_server_settings);
264 session.add_entity_request_handler(Self::handle_get_directory_environment);
265 session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
266 session.add_entity_request_handler(Self::handle_open_image_by_path);
267
268 session.add_entity_request_handler(BufferStore::handle_update_buffer);
269 session.add_entity_message_handler(BufferStore::handle_close_buffer);
270
271 session.add_request_handler(
272 extensions.downgrade(),
273 HeadlessExtensionStore::handle_sync_extensions,
274 );
275 session.add_request_handler(
276 extensions.downgrade(),
277 HeadlessExtensionStore::handle_install_extension,
278 );
279
280 BufferStore::init(&session);
281 WorktreeStore::init(&session);
282 SettingsObserver::init(&session);
283 LspStore::init(&session);
284 TaskStore::init(Some(&session));
285 ToolchainStore::init(&session);
286 DapStore::init(&session, cx);
287 // todo(debugger): Re init breakpoint store when we set it up for collab
288 // BreakpointStore::init(&client);
289 GitStore::init(&session);
290 AgentServerStore::init_headless(&session);
291
292 HeadlessProject {
293 next_entry_id: Default::default(),
294 session,
295 settings_observer,
296 fs,
297 worktree_store,
298 buffer_store,
299 lsp_store,
300 task_store,
301 dap_store,
302 agent_server_store,
303 languages,
304 extensions,
305 git_store,
306 environment,
307 _toolchain_store: toolchain_store,
308 }
309 }
310
311 fn on_buffer_event(
312 &mut self,
313 buffer: Entity<Buffer>,
314 event: &BufferEvent,
315 cx: &mut Context<Self>,
316 ) {
317 if let BufferEvent::Operation {
318 operation,
319 is_local: true,
320 } = event
321 {
322 cx.background_spawn(self.session.request(proto::UpdateBuffer {
323 project_id: REMOTE_SERVER_PROJECT_ID,
324 buffer_id: buffer.read(cx).remote_id().to_proto(),
325 operations: vec![serialize_operation(operation)],
326 }))
327 .detach()
328 }
329 }
330
331 fn on_lsp_store_event(
332 &mut self,
333 lsp_store: Entity<LspStore>,
334 event: &LspStoreEvent,
335 cx: &mut Context<Self>,
336 ) {
337 match event {
338 LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => {
339 let log_store = cx
340 .try_global::<GlobalLogStore>()
341 .map(|lsp_logs| lsp_logs.0.clone());
342 if let Some(log_store) = log_store {
343 log_store.update(cx, |log_store, cx| {
344 log_store.add_language_server(
345 LanguageServerKind::LocalSsh {
346 lsp_store: self.lsp_store.downgrade(),
347 },
348 *id,
349 Some(name.clone()),
350 *worktree_id,
351 lsp_store.read(cx).language_server_for_id(*id),
352 cx,
353 );
354 });
355 }
356 }
357 LspStoreEvent::LanguageServerRemoved(id) => {
358 let log_store = cx
359 .try_global::<GlobalLogStore>()
360 .map(|lsp_logs| lsp_logs.0.clone());
361 if let Some(log_store) = log_store {
362 log_store.update(cx, |log_store, cx| {
363 log_store.remove_language_server(*id, cx);
364 });
365 }
366 }
367 LspStoreEvent::LanguageServerUpdate {
368 language_server_id,
369 name,
370 message,
371 } => {
372 self.session
373 .send(proto::UpdateLanguageServer {
374 project_id: REMOTE_SERVER_PROJECT_ID,
375 server_name: name.as_ref().map(|name| name.to_string()),
376 language_server_id: language_server_id.to_proto(),
377 variant: Some(message.clone()),
378 })
379 .log_err();
380 }
381 LspStoreEvent::Notification(message) => {
382 self.session
383 .send(proto::Toast {
384 project_id: REMOTE_SERVER_PROJECT_ID,
385 notification_id: "lsp".to_string(),
386 message: message.clone(),
387 })
388 .log_err();
389 }
390 LspStoreEvent::LanguageServerPrompt(prompt) => {
391 let request = self.session.request(proto::LanguageServerPromptRequest {
392 project_id: REMOTE_SERVER_PROJECT_ID,
393 actions: prompt
394 .actions
395 .iter()
396 .map(|action| action.title.to_string())
397 .collect(),
398 level: Some(prompt_to_proto(prompt)),
399 lsp_name: prompt.lsp_name.clone(),
400 message: prompt.message.clone(),
401 });
402 let prompt = prompt.clone();
403 cx.background_spawn(async move {
404 let response = request.await?;
405 if let Some(action_response) = response.action_response {
406 prompt.respond(action_response as usize).await;
407 }
408 anyhow::Ok(())
409 })
410 .detach();
411 }
412 _ => {}
413 }
414 }
415
416 pub async fn handle_add_worktree(
417 this: Entity<Self>,
418 message: TypedEnvelope<proto::AddWorktree>,
419 mut cx: AsyncApp,
420 ) -> Result<proto::AddWorktreeResponse> {
421 use client::ErrorCodeExt;
422 let fs = this.read_with(&cx, |this, _| this.fs.clone())?;
423 let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string());
424
425 let canonicalized = match fs.canonicalize(&path).await {
426 Ok(path) => path,
427 Err(e) => {
428 let mut parent = path
429 .parent()
430 .ok_or(e)
431 .with_context(|| format!("{path:?} does not exist"))?;
432 if parent == Path::new("") {
433 parent = util::paths::home_dir();
434 }
435 let parent = fs.canonicalize(parent).await.map_err(|_| {
436 anyhow!(
437 proto::ErrorCode::DevServerProjectPathDoesNotExist
438 .with_tag("path", path.to_string_lossy().as_ref())
439 )
440 })?;
441 parent.join(path.file_name().unwrap())
442 }
443 };
444
445 let worktree = this
446 .read_with(&cx.clone(), |this, _| {
447 Worktree::local(
448 Arc::from(canonicalized.as_path()),
449 message.payload.visible,
450 this.fs.clone(),
451 this.next_entry_id.clone(),
452 &mut cx,
453 )
454 })?
455 .await?;
456
457 let response = this.read_with(&cx, |_, cx| {
458 let worktree = worktree.read(cx);
459 proto::AddWorktreeResponse {
460 worktree_id: worktree.id().to_proto(),
461 canonicalized_path: canonicalized.to_string_lossy().into_owned(),
462 }
463 })?;
464
465 // We spawn this asynchronously, so that we can send the response back
466 // *before* `worktree_store.add()` can send out UpdateProject requests
467 // to the client about the new worktree.
468 //
469 // That lets the client manage the reference/handles of the newly-added
470 // worktree, before getting interrupted by an UpdateProject request.
471 //
472 // This fixes the problem of the client sending the AddWorktree request,
473 // headless project sending out a project update, client receiving it
474 // and immediately dropping the reference of the new client, causing it
475 // to be dropped on the headless project, and the client only then
476 // receiving a response to AddWorktree.
477 cx.spawn(async move |cx| {
478 this.update(cx, |this, cx| {
479 this.worktree_store.update(cx, |worktree_store, cx| {
480 worktree_store.add(&worktree, cx);
481 });
482 })
483 .log_err();
484 })
485 .detach();
486
487 Ok(response)
488 }
489
490 pub async fn handle_remove_worktree(
491 this: Entity<Self>,
492 envelope: TypedEnvelope<proto::RemoveWorktree>,
493 mut cx: AsyncApp,
494 ) -> Result<proto::Ack> {
495 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
496 this.update(&mut cx, |this, cx| {
497 this.worktree_store.update(cx, |worktree_store, cx| {
498 worktree_store.remove_worktree(worktree_id, cx);
499 });
500 })?;
501 Ok(proto::Ack {})
502 }
503
504 pub async fn handle_open_buffer_by_path(
505 this: Entity<Self>,
506 message: TypedEnvelope<proto::OpenBufferByPath>,
507 mut cx: AsyncApp,
508 ) -> Result<proto::OpenBufferResponse> {
509 let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
510 let path = RelPath::from_proto(&message.payload.path)?;
511 let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
512 let buffer_store = this.buffer_store.clone();
513 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
514 buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx)
515 });
516 anyhow::Ok((buffer_store, buffer))
517 })??;
518
519 let buffer = buffer.await?;
520 let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
521 buffer_store.update(&mut cx, |buffer_store, cx| {
522 buffer_store
523 .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
524 .detach_and_log_err(cx);
525 })?;
526
527 Ok(proto::OpenBufferResponse {
528 buffer_id: buffer_id.to_proto(),
529 })
530 }
531
532 pub async fn handle_open_image_by_path(
533 this: Entity<Self>,
534 message: TypedEnvelope<proto::OpenImageByPath>,
535 mut cx: AsyncApp,
536 ) -> Result<proto::OpenImageResponse> {
537 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
538 let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
539 let path = RelPath::from_proto(&message.payload.path)?;
540 let project_id = message.payload.project_id;
541 use proto::create_image_for_peer::Variant;
542
543 let (worktree_store, session) = this.read_with(&cx, |this, _| {
544 (this.worktree_store.clone(), this.session.clone())
545 })?;
546
547 let worktree = worktree_store
548 .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))?
549 .context("worktree not found")?;
550
551 let load_task = worktree.update(&mut cx, |worktree, cx| {
552 worktree.load_binary_file(path.as_ref(), cx)
553 })?;
554
555 let loaded_file = load_task.await?;
556 let content = loaded_file.content;
557 let file = loaded_file.file;
558
559 let proto_file = worktree.read_with(&cx, |_worktree, cx| file.to_proto(cx))?;
560 let image_id =
561 ImageId::from(NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed)).unwrap());
562
563 let format = image::guess_format(&content)
564 .map(|f| format!("{:?}", f).to_lowercase())
565 .unwrap_or_else(|_| "unknown".to_string());
566
567 let state = proto::ImageState {
568 id: image_id.to_proto(),
569 file: Some(proto_file),
570 content_size: content.len() as u64,
571 format,
572 };
573
574 session.send(proto::CreateImageForPeer {
575 project_id,
576 peer_id: Some(REMOTE_SERVER_PEER_ID),
577 variant: Some(Variant::State(state)),
578 })?;
579
580 const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks
581 for chunk in content.chunks(CHUNK_SIZE) {
582 session.send(proto::CreateImageForPeer {
583 project_id,
584 peer_id: Some(REMOTE_SERVER_PEER_ID),
585 variant: Some(Variant::Chunk(proto::ImageChunk {
586 image_id: image_id.to_proto(),
587 data: chunk.to_vec(),
588 })),
589 })?;
590 }
591
592 Ok(proto::OpenImageResponse {
593 image_id: image_id.to_proto(),
594 })
595 }
596
597 pub async fn handle_open_new_buffer(
598 this: Entity<Self>,
599 _message: TypedEnvelope<proto::OpenNewBuffer>,
600 mut cx: AsyncApp,
601 ) -> Result<proto::OpenBufferResponse> {
602 let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
603 let buffer_store = this.buffer_store.clone();
604 let buffer = this
605 .buffer_store
606 .update(cx, |buffer_store, cx| buffer_store.create_buffer(true, cx));
607 anyhow::Ok((buffer_store, buffer))
608 })??;
609
610 let buffer = buffer.await?;
611 let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
612 buffer_store.update(&mut cx, |buffer_store, cx| {
613 buffer_store
614 .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
615 .detach_and_log_err(cx);
616 })?;
617
618 Ok(proto::OpenBufferResponse {
619 buffer_id: buffer_id.to_proto(),
620 })
621 }
622
623 async fn handle_toggle_lsp_logs(
624 _: Entity<Self>,
625 envelope: TypedEnvelope<proto::ToggleLspLogs>,
626 cx: AsyncApp,
627 ) -> Result<()> {
628 let server_id = LanguageServerId::from_proto(envelope.payload.server_id);
629 cx.update(|cx| {
630 let log_store = cx
631 .try_global::<GlobalLogStore>()
632 .map(|global_log_store| global_log_store.0.clone())
633 .context("lsp logs store is missing")?;
634 let toggled_log_kind =
635 match proto::toggle_lsp_logs::LogType::from_i32(envelope.payload.log_type)
636 .context("invalid log type")?
637 {
638 proto::toggle_lsp_logs::LogType::Log => LogKind::Logs,
639 proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace,
640 proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc,
641 };
642 log_store.update(cx, |log_store, _| {
643 log_store.toggle_lsp_logs(server_id, envelope.payload.enabled, toggled_log_kind);
644 });
645 anyhow::Ok(())
646 })??;
647
648 Ok(())
649 }
650
651 async fn handle_open_server_settings(
652 this: Entity<Self>,
653 _: TypedEnvelope<proto::OpenServerSettings>,
654 mut cx: AsyncApp,
655 ) -> Result<proto::OpenBufferResponse> {
656 let settings_path = paths::settings_file();
657 let (worktree, path) = this
658 .update(&mut cx, |this, cx| {
659 this.worktree_store.update(cx, |worktree_store, cx| {
660 worktree_store.find_or_create_worktree(settings_path, false, cx)
661 })
662 })?
663 .await?;
664
665 let (buffer, buffer_store) = this.update(&mut cx, |this, cx| {
666 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
667 buffer_store.open_buffer(
668 ProjectPath {
669 worktree_id: worktree.read(cx).id(),
670 path: path,
671 },
672 cx,
673 )
674 });
675
676 (buffer, this.buffer_store.clone())
677 })?;
678
679 let buffer = buffer.await?;
680
681 let buffer_id = cx.update(|cx| {
682 if buffer.read(cx).is_empty() {
683 buffer.update(cx, |buffer, cx| {
684 buffer.edit([(0..0, initial_server_settings_content())], None, cx)
685 });
686 }
687
688 let buffer_id = buffer.read(cx).remote_id();
689
690 buffer_store.update(cx, |buffer_store, cx| {
691 buffer_store
692 .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
693 .detach_and_log_err(cx);
694 });
695
696 buffer_id
697 })?;
698
699 Ok(proto::OpenBufferResponse {
700 buffer_id: buffer_id.to_proto(),
701 })
702 }
703
704 async fn handle_find_search_candidates(
705 this: Entity<Self>,
706 envelope: TypedEnvelope<proto::FindSearchCandidates>,
707 mut cx: AsyncApp,
708 ) -> Result<proto::FindSearchCandidatesResponse> {
709 let message = envelope.payload;
710 let query = SearchQuery::from_proto(
711 message.query.context("missing query field")?,
712 PathStyle::local(),
713 )?;
714 let results = this.update(&mut cx, |this, cx| {
715 project::Search::local(
716 this.fs.clone(),
717 this.buffer_store.clone(),
718 this.worktree_store.clone(),
719 message.limit as _,
720 cx,
721 )
722 .into_handle(query, cx)
723 .matching_buffers(cx)
724 })?;
725
726 let mut response = proto::FindSearchCandidatesResponse {
727 buffer_ids: Vec::new(),
728 };
729
730 let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
731
732 while let Ok(buffer) = results.recv().await {
733 let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?;
734 response.buffer_ids.push(buffer_id.to_proto());
735 buffer_store
736 .update(&mut cx, |buffer_store, cx| {
737 buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
738 })?
739 .await?;
740 }
741
742 Ok(response)
743 }
744
745 async fn handle_list_remote_directory(
746 this: Entity<Self>,
747 envelope: TypedEnvelope<proto::ListRemoteDirectory>,
748 cx: AsyncApp,
749 ) -> Result<proto::ListRemoteDirectoryResponse> {
750 let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
751 let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
752 let check_info = envelope
753 .payload
754 .config
755 .as_ref()
756 .is_some_and(|config| config.is_dir);
757
758 let mut entries = Vec::new();
759 let mut entry_info = Vec::new();
760 let mut response = fs.read_dir(&expanded).await?;
761 while let Some(path) = response.next().await {
762 let path = path?;
763 if let Some(file_name) = path.file_name() {
764 entries.push(file_name.to_string_lossy().into_owned());
765 if check_info {
766 let is_dir = fs.is_dir(&path).await;
767 entry_info.push(proto::EntryInfo { is_dir });
768 }
769 }
770 }
771 Ok(proto::ListRemoteDirectoryResponse {
772 entries,
773 entry_info,
774 })
775 }
776
777 async fn handle_get_path_metadata(
778 this: Entity<Self>,
779 envelope: TypedEnvelope<proto::GetPathMetadata>,
780 cx: AsyncApp,
781 ) -> Result<proto::GetPathMetadataResponse> {
782 let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
783 let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
784
785 let metadata = fs.metadata(&expanded).await?;
786 let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
787
788 Ok(proto::GetPathMetadataResponse {
789 exists: metadata.is_some(),
790 is_dir,
791 path: expanded.to_string_lossy().into_owned(),
792 })
793 }
794
795 async fn handle_shutdown_remote_server(
796 _this: Entity<Self>,
797 _envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
798 cx: AsyncApp,
799 ) -> Result<proto::Ack> {
800 cx.spawn(async move |cx| {
801 cx.update(|cx| {
802 // TODO: This is a hack, because in a headless project, shutdown isn't executed
803 // when calling quit, but it should be.
804 cx.shutdown();
805 cx.quit();
806 })
807 })
808 .detach();
809
810 Ok(proto::Ack {})
811 }
812
813 pub async fn handle_ping(
814 _this: Entity<Self>,
815 _envelope: TypedEnvelope<proto::Ping>,
816 _cx: AsyncApp,
817 ) -> Result<proto::Ack> {
818 log::debug!("Received ping from client");
819 Ok(proto::Ack {})
820 }
821
822 async fn handle_get_processes(
823 _this: Entity<Self>,
824 _envelope: TypedEnvelope<proto::GetProcesses>,
825 _cx: AsyncApp,
826 ) -> Result<proto::GetProcessesResponse> {
827 let mut processes = Vec::new();
828 let refresh_kind = RefreshKind::nothing().with_processes(
829 ProcessRefreshKind::nothing()
830 .without_tasks()
831 .with_cmd(UpdateKind::Always),
832 );
833
834 for process in System::new_with_specifics(refresh_kind)
835 .processes()
836 .values()
837 {
838 let name = process.name().to_string_lossy().into_owned();
839 let command = process
840 .cmd()
841 .iter()
842 .map(|s| s.to_string_lossy().into_owned())
843 .collect::<Vec<_>>();
844
845 processes.push(proto::ProcessInfo {
846 pid: process.pid().as_u32(),
847 name,
848 command,
849 });
850 }
851
852 processes.sort_by_key(|p| p.name.clone());
853
854 Ok(proto::GetProcessesResponse { processes })
855 }
856
857 async fn handle_get_directory_environment(
858 this: Entity<Self>,
859 envelope: TypedEnvelope<proto::GetDirectoryEnvironment>,
860 mut cx: AsyncApp,
861 ) -> Result<proto::DirectoryEnvironment> {
862 let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?;
863 let directory = PathBuf::from(envelope.payload.directory);
864 let environment = this
865 .update(&mut cx, |this, cx| {
866 this.environment.update(cx, |environment, cx| {
867 environment.local_directory_environment(&shell, directory.into(), cx)
868 })
869 })?
870 .await
871 .context("failed to get directory environment")?
872 .into_iter()
873 .collect();
874 Ok(proto::DirectoryEnvironment { environment })
875 }
876}
877
878fn prompt_to_proto(
879 prompt: &project::LanguageServerPromptRequest,
880) -> proto::language_server_prompt_request::Level {
881 match prompt.level {
882 PromptLevel::Info => proto::language_server_prompt_request::Level::Info(
883 proto::language_server_prompt_request::Info {},
884 ),
885 PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning(
886 proto::language_server_prompt_request::Warning {},
887 ),
888 PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical(
889 proto::language_server_prompt_request::Critical {},
890 ),
891 }
892}