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},
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 mut cx: AsyncApp,
627 ) -> Result<()> {
628 let server_id = LanguageServerId::from_proto(envelope.payload.server_id);
629 let lsp_logs = cx
630 .update(|cx| {
631 cx.try_global::<GlobalLogStore>()
632 .map(|lsp_logs| lsp_logs.0.clone())
633 })?
634 .context("lsp logs store is missing")?;
635
636 lsp_logs.update(&mut cx, |lsp_logs, _| {
637 // RPC logs are very noisy and we need to toggle it on the headless server too.
638 // The rest of the logs for the ssh project are very important to have toggled always,
639 // to e.g. send language server error logs to the client before anything is toggled.
640 if envelope.payload.enabled {
641 lsp_logs.enable_rpc_trace_for_language_server(server_id);
642 } else {
643 lsp_logs.disable_rpc_trace_for_language_server(server_id);
644 }
645 })?;
646 Ok(())
647 }
648
649 async fn handle_open_server_settings(
650 this: Entity<Self>,
651 _: TypedEnvelope<proto::OpenServerSettings>,
652 mut cx: AsyncApp,
653 ) -> Result<proto::OpenBufferResponse> {
654 let settings_path = paths::settings_file();
655 let (worktree, path) = this
656 .update(&mut cx, |this, cx| {
657 this.worktree_store.update(cx, |worktree_store, cx| {
658 worktree_store.find_or_create_worktree(settings_path, false, cx)
659 })
660 })?
661 .await?;
662
663 let (buffer, buffer_store) = this.update(&mut cx, |this, cx| {
664 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
665 buffer_store.open_buffer(
666 ProjectPath {
667 worktree_id: worktree.read(cx).id(),
668 path: path,
669 },
670 cx,
671 )
672 });
673
674 (buffer, this.buffer_store.clone())
675 })?;
676
677 let buffer = buffer.await?;
678
679 let buffer_id = cx.update(|cx| {
680 if buffer.read(cx).is_empty() {
681 buffer.update(cx, |buffer, cx| {
682 buffer.edit([(0..0, initial_server_settings_content())], None, cx)
683 });
684 }
685
686 let buffer_id = buffer.read(cx).remote_id();
687
688 buffer_store.update(cx, |buffer_store, cx| {
689 buffer_store
690 .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
691 .detach_and_log_err(cx);
692 });
693
694 buffer_id
695 })?;
696
697 Ok(proto::OpenBufferResponse {
698 buffer_id: buffer_id.to_proto(),
699 })
700 }
701
702 async fn handle_find_search_candidates(
703 this: Entity<Self>,
704 envelope: TypedEnvelope<proto::FindSearchCandidates>,
705 mut cx: AsyncApp,
706 ) -> Result<proto::FindSearchCandidatesResponse> {
707 let message = envelope.payload;
708 let query = SearchQuery::from_proto(
709 message.query.context("missing query field")?,
710 PathStyle::local(),
711 )?;
712 let results = this.update(&mut cx, |this, cx| {
713 this.buffer_store.update(cx, |buffer_store, cx| {
714 buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx)
715 })
716 })?;
717
718 let mut response = proto::FindSearchCandidatesResponse {
719 buffer_ids: Vec::new(),
720 };
721
722 let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?;
723
724 while let Ok(buffer) = results.recv().await {
725 let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?;
726 response.buffer_ids.push(buffer_id.to_proto());
727 buffer_store
728 .update(&mut cx, |buffer_store, cx| {
729 buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx)
730 })?
731 .await?;
732 }
733
734 Ok(response)
735 }
736
737 async fn handle_list_remote_directory(
738 this: Entity<Self>,
739 envelope: TypedEnvelope<proto::ListRemoteDirectory>,
740 cx: AsyncApp,
741 ) -> Result<proto::ListRemoteDirectoryResponse> {
742 let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
743 let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
744 let check_info = envelope
745 .payload
746 .config
747 .as_ref()
748 .is_some_and(|config| config.is_dir);
749
750 let mut entries = Vec::new();
751 let mut entry_info = Vec::new();
752 let mut response = fs.read_dir(&expanded).await?;
753 while let Some(path) = response.next().await {
754 let path = path?;
755 if let Some(file_name) = path.file_name() {
756 entries.push(file_name.to_string_lossy().into_owned());
757 if check_info {
758 let is_dir = fs.is_dir(&path).await;
759 entry_info.push(proto::EntryInfo { is_dir });
760 }
761 }
762 }
763 Ok(proto::ListRemoteDirectoryResponse {
764 entries,
765 entry_info,
766 })
767 }
768
769 async fn handle_get_path_metadata(
770 this: Entity<Self>,
771 envelope: TypedEnvelope<proto::GetPathMetadata>,
772 cx: AsyncApp,
773 ) -> Result<proto::GetPathMetadataResponse> {
774 let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
775 let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string());
776
777 let metadata = fs.metadata(&expanded).await?;
778 let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
779
780 Ok(proto::GetPathMetadataResponse {
781 exists: metadata.is_some(),
782 is_dir,
783 path: expanded.to_string_lossy().into_owned(),
784 })
785 }
786
787 async fn handle_shutdown_remote_server(
788 _this: Entity<Self>,
789 _envelope: TypedEnvelope<proto::ShutdownRemoteServer>,
790 cx: AsyncApp,
791 ) -> Result<proto::Ack> {
792 cx.spawn(async move |cx| {
793 cx.update(|cx| {
794 // TODO: This is a hack, because in a headless project, shutdown isn't executed
795 // when calling quit, but it should be.
796 cx.shutdown();
797 cx.quit();
798 })
799 })
800 .detach();
801
802 Ok(proto::Ack {})
803 }
804
805 pub async fn handle_ping(
806 _this: Entity<Self>,
807 _envelope: TypedEnvelope<proto::Ping>,
808 _cx: AsyncApp,
809 ) -> Result<proto::Ack> {
810 log::debug!("Received ping from client");
811 Ok(proto::Ack {})
812 }
813
814 async fn handle_get_processes(
815 _this: Entity<Self>,
816 _envelope: TypedEnvelope<proto::GetProcesses>,
817 _cx: AsyncApp,
818 ) -> Result<proto::GetProcessesResponse> {
819 let mut processes = Vec::new();
820 let refresh_kind = RefreshKind::nothing().with_processes(
821 ProcessRefreshKind::nothing()
822 .without_tasks()
823 .with_cmd(UpdateKind::Always),
824 );
825
826 for process in System::new_with_specifics(refresh_kind)
827 .processes()
828 .values()
829 {
830 let name = process.name().to_string_lossy().into_owned();
831 let command = process
832 .cmd()
833 .iter()
834 .map(|s| s.to_string_lossy().into_owned())
835 .collect::<Vec<_>>();
836
837 processes.push(proto::ProcessInfo {
838 pid: process.pid().as_u32(),
839 name,
840 command,
841 });
842 }
843
844 processes.sort_by_key(|p| p.name.clone());
845
846 Ok(proto::GetProcessesResponse { processes })
847 }
848
849 async fn handle_get_directory_environment(
850 this: Entity<Self>,
851 envelope: TypedEnvelope<proto::GetDirectoryEnvironment>,
852 mut cx: AsyncApp,
853 ) -> Result<proto::DirectoryEnvironment> {
854 let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?;
855 let directory = PathBuf::from(envelope.payload.directory);
856 let environment = this
857 .update(&mut cx, |this, cx| {
858 this.environment.update(cx, |environment, cx| {
859 environment.local_directory_environment(&shell, directory.into(), cx)
860 })
861 })?
862 .await
863 .context("failed to get directory environment")?
864 .into_iter()
865 .collect();
866 Ok(proto::DirectoryEnvironment { environment })
867 }
868}
869
870fn prompt_to_proto(
871 prompt: &project::LanguageServerPromptRequest,
872) -> proto::language_server_prompt_request::Level {
873 match prompt.level {
874 PromptLevel::Info => proto::language_server_prompt_request::Level::Info(
875 proto::language_server_prompt_request::Info {},
876 ),
877 PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning(
878 proto::language_server_prompt_request::Warning {},
879 ),
880 PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical(
881 proto::language_server_prompt_request::Critical {},
882 ),
883 }
884}