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