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