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