Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:43:34.448Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

remote_editing_tests.rs

4311 lines · 131.2 KB · rust
1/// todo(windows)
2/// The tests in this file assume that server_cx is running on Windows too.
3/// We neead to find a way to test Windows-Non-Windows interactions.
4use crate::headless_project::HeadlessProject;
5use agent::{
6    AgentTool, NativeAgent, NativeAgentConnection, ReadFileTool, ReadFileToolInput, SkillTool,
7    SkillToolInput, SkillToolOutput, Templates, ThreadStore, ToolCallEventStream, ToolInput,
8    skill_body_resolver_for_project, skills_resolver_for_project,
9};
10use client::{Client, UserStore};
11use clock::FakeSystemClock;
12use collections::{HashMap, HashSet};
13use language_model::{LanguageModelRegistry, LanguageModelToolResultContent};
14use languages::rust_lang;
15
16use editor::{
17    Editor, SelectionEffects,
18    actions::{ConfirmCodeAction, ToggleCodeActions},
19    code_context_menus::CodeContextMenu,
20};
21use extension::ExtensionHostProxy;
22use fs::{FakeFs, Fs};
23use git::{
24    Oid,
25    repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree},
26};
27use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext};
28use http_client::{BlockedHttpClient, FakeHttpClient};
29use language::{
30    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point,
31    language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings},
32};
33use lsp::{
34    CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
35    LanguageServerName,
36};
37use node_runtime::NodeRuntime;
38use project::{
39    ProgressToken, Project,
40    agent_server_store::AgentServerCommand,
41    search::{SearchQuery, SearchResult},
42};
43use remote::RemoteClient;
44use rpc::proto;
45use serde_json::json;
46use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
47use smol::stream::StreamExt;
48use std::{
49    path::{Path, PathBuf},
50    rc::Rc,
51    str::FromStr,
52    sync::{
53        Arc,
54        atomic::{AtomicUsize, Ordering},
55    },
56};
57use unindent::Unindent as _;
58use util::{path, path_list::PathList, paths::PathMatcher, rel_path::rel_path};
59
60#[gpui::test]
61async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
62    let fs = FakeFs::new(server_cx.executor());
63    fs.insert_tree(
64        path!("/code"),
65        json!({
66            "project1": {
67                ".git": {},
68                "README.md": "# project 1",
69                "src": {
70                    "lib.rs": "fn one() -> usize { 1 }"
71                }
72            },
73            "project2": {
74                "README.md": "# project 2",
75            },
76        }),
77    )
78    .await;
79    fs.set_index_for_repo(
80        Path::new(path!("/code/project1/.git")),
81        &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
82    );
83
84    let (project, _headless) = init_test(&fs, cx, server_cx).await;
85    let (worktree, _) = project
86        .update(cx, |project, cx| {
87            project.find_or_create_worktree(path!("/code/project1"), true, cx)
88        })
89        .await
90        .unwrap();
91
92    // The client sees the worktree's contents.
93    cx.executor().run_until_parked();
94    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
95    worktree.update(cx, |worktree, _cx| {
96        assert_eq!(
97            worktree.paths().collect::<Vec<_>>(),
98            vec![
99                rel_path("README.md"),
100                rel_path("src"),
101                rel_path("src/lib.rs"),
102            ]
103        );
104    });
105
106    // The user opens a buffer in the remote worktree. The buffer's
107    // contents are loaded from the remote filesystem.
108    let buffer = project
109        .update(cx, |project, cx| {
110            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
111        })
112        .await
113        .unwrap();
114    let diff = project
115        .update(cx, |project, cx| {
116            project.open_unstaged_diff(buffer.clone(), cx)
117        })
118        .await
119        .unwrap();
120
121    diff.update(cx, |diff, cx| {
122        assert_eq!(
123            diff.base_text_string(cx).unwrap(),
124            "fn one() -> usize { 0 }"
125        );
126    });
127
128    buffer.update(cx, |buffer, cx| {
129        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
130        let ix = buffer.text().find('1').unwrap();
131        buffer.edit([(ix..ix + 1, "100")], None, cx);
132    });
133
134    // The user saves the buffer. The new contents are written to the
135    // remote filesystem.
136    project
137        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
138        .await
139        .unwrap();
140    assert_eq!(
141        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
142        "fn one() -> usize { 100 }"
143    );
144
145    // A new file is created in the remote filesystem. The user
146    // sees the new file.
147    fs.save(
148        path!("/code/project1/src/main.rs").as_ref(),
149        &"fn main() {}".into(),
150        Default::default(),
151    )
152    .await
153    .unwrap();
154    cx.executor().run_until_parked();
155    worktree.update(cx, |worktree, _cx| {
156        assert_eq!(
157            worktree.paths().collect::<Vec<_>>(),
158            vec![
159                rel_path("README.md"),
160                rel_path("src"),
161                rel_path("src/lib.rs"),
162                rel_path("src/main.rs"),
163            ]
164        );
165    });
166
167    // A file that is currently open in a buffer is renamed.
168    fs.rename(
169        path!("/code/project1/src/lib.rs").as_ref(),
170        path!("/code/project1/src/lib2.rs").as_ref(),
171        Default::default(),
172    )
173    .await
174    .unwrap();
175    cx.executor().run_until_parked();
176    buffer.update(cx, |buffer, _| {
177        assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
178    });
179
180    fs.set_index_for_repo(
181        Path::new(path!("/code/project1/.git")),
182        &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
183    );
184    cx.executor().run_until_parked();
185    diff.update(cx, |diff, cx| {
186        assert_eq!(
187            diff.base_text_string(cx).unwrap(),
188            "fn one() -> usize { 100 }"
189        );
190    });
191}
192
193#[gpui::test]
194async fn test_remote_telemetry_event_forwarding(
195    cx: &mut TestAppContext,
196    server_cx: &mut TestAppContext,
197) {
198    // This mirrors `init_test`, but retains the server-side session so the test
199    // can drive a forwarded telemetry event over the proto channel (as
200    // `init_telemetry_forwarding` does on a real remote server).
201    let server_fs = FakeFs::new(server_cx.executor());
202    server_fs
203        .insert_tree(
204            path!("/code"),
205            json!({ "project1": { "README.md": "# project 1" } }),
206        )
207        .await;
208
209    cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx));
210    server_cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx));
211    init_logger();
212
213    let (opts, server_session, _) = RemoteClient::fake_server(cx, server_cx);
214    let http_client = Arc::new(BlockedHttpClient);
215    let node_runtime = NodeRuntime::unavailable();
216    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
217    let proxy = Arc::new(ExtensionHostProxy::new());
218    server_cx.update(HeadlessProject::init);
219    let headless = server_cx.new(|cx| {
220        HeadlessProject::new(
221            crate::HeadlessAppState {
222                session: server_session.clone(),
223                fs: server_fs.clone(),
224                http_client,
225                node_runtime,
226                languages,
227                extension_host_proxy: proxy,
228                startup_time: std::time::Instant::now(),
229            },
230            false,
231            cx,
232        )
233    });
234
235    let ssh = RemoteClient::connect_mock(opts, cx).await;
236    let project = build_project(ssh, cx);
237    project
238        .update(cx, {
239            let headless = headless.clone();
240            |_, cx| cx.on_release(|_, _| drop(headless))
241        })
242        .detach();
243
244    // The remote server forwards a bare `FlexibleEvent` as JSON; mirror that
245    // here by sending the proto message the forwarding task would send.
246    let event_json = json!({
247        "event_type": "fs_watcher_poll",
248        "event_properties": { "path": "/code/project1" },
249    })
250    .to_string();
251    server_session
252        .send(proto::TelemetryEvent {
253            project_id: proto::REMOTE_SERVER_PROJECT_ID,
254            event_json,
255        })
256        .unwrap();
257    cx.executor().run_until_parked();
258
259    let events = project.read_with(cx, |project, _| {
260        project.client().telemetry().queued_events()
261    });
262    assert_eq!(
263        events.len(),
264        1,
265        "the forwarded event should be reported once"
266    );
267    let event = &events[0];
268    assert_eq!(event.event_type, "fs_watcher_poll");
269    // The event's original properties survive the round-trip.
270    assert_eq!(
271        event.event_properties.get("path"),
272        Some(&serde_json::Value::String("/code/project1".to_string()))
273    );
274    // The client stamps the remote host metadata it learned at connection time.
275    // The mock connection reports a Linux/x86_64 host over a "mock" connection.
276    assert_eq!(
277        event.event_properties.get("remote"),
278        Some(&serde_json::Value::Bool(true))
279    );
280    assert_eq!(
281        event.event_properties.get("remote_connection_type"),
282        Some(&serde_json::Value::String("mock".to_string()))
283    );
284    assert_eq!(
285        event.event_properties.get("remote_os_name"),
286        Some(&serde_json::Value::String("Linux".to_string()))
287    );
288    assert_eq!(
289        event.event_properties.get("remote_architecture"),
290        Some(&serde_json::Value::String("x86_64".to_string()))
291    );
292}
293
294async fn do_search_and_assert(
295    project: &Entity<Project>,
296    query: &str,
297    files_to_include: PathMatcher,
298    match_full_paths: bool,
299    expected_paths: &[&str],
300    mut cx: TestAppContext,
301) -> Vec<Entity<Buffer>> {
302    let query = query.to_string();
303    let receiver = project.update(&mut cx, |project, cx| {
304        project.search(
305            SearchQuery::text(
306                query,
307                false,
308                true,
309                false,
310                files_to_include,
311                Default::default(),
312                match_full_paths,
313                None,
314            )
315            .unwrap(),
316            cx,
317        )
318    });
319
320    let mut buffers = Vec::new();
321    for expected_path in expected_paths {
322        let buffer = loop {
323            let response = receiver.rx.recv().await.unwrap();
324            match response {
325                SearchResult::Buffer { buffer, .. } => break buffer,
326                SearchResult::LimitReached => panic!("incorrect result"),
327                SearchResult::WaitingForScan | SearchResult::Searching => continue,
328            }
329        };
330        buffer.update(&mut cx, |buffer, cx| {
331            assert_eq!(
332                buffer.file().unwrap().full_path(cx).to_string_lossy(),
333                *expected_path
334            )
335        });
336        buffers.push(buffer);
337    }
338
339    assert!(receiver.rx.recv().await.is_err());
340    buffers
341}
342
343#[gpui::test]
344async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
345    let fs = FakeFs::new(server_cx.executor());
346    fs.insert_tree(
347        path!("/code"),
348        json!({
349            "project1": {
350                ".git": {},
351                "README.md": "# project 1",
352                "src": {
353                    "lib.rs": "fn one() -> usize { 1 }"
354                }
355            },
356        }),
357    )
358    .await;
359
360    let (project, headless) = init_test(&fs, cx, server_cx).await;
361
362    project
363        .update(cx, |project, cx| {
364            project.find_or_create_worktree(path!("/code/project1"), true, cx)
365        })
366        .await
367        .unwrap();
368
369    cx.run_until_parked();
370
371    let buffers = do_search_and_assert(
372        &project,
373        "project",
374        Default::default(),
375        false,
376        &[path!("project1/README.md")],
377        cx.clone(),
378    )
379    .await;
380    let buffer = buffers.into_iter().next().unwrap();
381
382    // test that the headless server is tracking which buffers we have open correctly.
383    cx.run_until_parked();
384    headless.update(server_cx, |headless, cx| {
385        assert!(headless.buffer_store.read(cx).has_shared_buffers())
386    });
387    do_search_and_assert(
388        &project,
389        "project",
390        Default::default(),
391        false,
392        &[path!("project1/README.md")],
393        cx.clone(),
394    )
395    .await;
396    server_cx.run_until_parked();
397    cx.update(|_| {
398        drop(buffer);
399    });
400    cx.run_until_parked();
401    server_cx.run_until_parked();
402    headless.update(server_cx, |headless, cx| {
403        assert!(!headless.buffer_store.read(cx).has_shared_buffers())
404    });
405
406    do_search_and_assert(
407        &project,
408        "project",
409        Default::default(),
410        false,
411        &[path!("project1/README.md")],
412        cx.clone(),
413    )
414    .await;
415}
416
417#[gpui::test]
418async fn test_remote_project_search_single_cpu(
419    cx: &mut TestAppContext,
420    server_cx: &mut TestAppContext,
421) {
422    let fs = FakeFs::new(server_cx.executor());
423    fs.insert_tree(
424        path!("/code"),
425        json!({
426            "project1": {
427                ".git": {},
428                "README.md": "# project 1",
429                "src": {
430                    "lib.rs": "fn one() -> usize { 1 }"
431                }
432            },
433        }),
434    )
435    .await;
436
437    // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
438    // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
439    // which silently drops all search channels and produces zero results.
440    server_cx.executor().set_num_cpus(1);
441
442    let (project, _) = init_test(&fs, cx, server_cx).await;
443
444    project
445        .update(cx, |project, cx| {
446            project.find_or_create_worktree(path!("/code/project1"), true, cx)
447        })
448        .await
449        .unwrap();
450
451    cx.run_until_parked();
452
453    do_search_and_assert(
454        &project,
455        "project",
456        Default::default(),
457        false,
458        &[path!("project1/README.md")],
459        cx.clone(),
460    )
461    .await;
462}
463
464#[gpui::test]
465async fn test_remote_project_search_inclusion(
466    cx: &mut TestAppContext,
467    server_cx: &mut TestAppContext,
468) {
469    let fs = FakeFs::new(server_cx.executor());
470    fs.insert_tree(
471        path!("/code"),
472        json!({
473            "project1": {
474                "README.md": "# project 1",
475            },
476            "project2": {
477                "README.md": "# project 2",
478            },
479        }),
480    )
481    .await;
482
483    let (project, _) = init_test(&fs, cx, server_cx).await;
484
485    project
486        .update(cx, |project, cx| {
487            project.find_or_create_worktree(path!("/code/project1"), true, cx)
488        })
489        .await
490        .unwrap();
491
492    project
493        .update(cx, |project, cx| {
494            project.find_or_create_worktree(path!("/code/project2"), true, cx)
495        })
496        .await
497        .unwrap();
498
499    cx.run_until_parked();
500
501    // Case 1: Test search with path matcher limiting to only one worktree
502    let path_matcher = PathMatcher::new(
503        &["project1/*.md".to_owned()],
504        util::paths::PathStyle::local(),
505    )
506    .unwrap();
507    do_search_and_assert(
508        &project,
509        "project",
510        path_matcher,
511        true, // should be true in case of multiple worktrees
512        &[path!("project1/README.md")],
513        cx.clone(),
514    )
515    .await;
516
517    // Case 2: Test search without path matcher, matching both worktrees
518    do_search_and_assert(
519        &project,
520        "project",
521        Default::default(),
522        true, // should be true in case of multiple worktrees
523        &[path!("project1/README.md"), path!("project2/README.md")],
524        cx.clone(),
525    )
526    .await;
527}
528
529#[gpui::test]
530async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
531    let fs = FakeFs::new(server_cx.executor());
532    fs.insert_tree(
533        "/code",
534        json!({
535            "project1": {
536                ".git": {},
537                "README.md": "# project 1",
538                "src": {
539                    "lib.rs": "fn one() -> usize { 1 }"
540                }
541            },
542        }),
543    )
544    .await;
545
546    let (project, headless) = init_test(&fs, cx, server_cx).await;
547
548    cx.update_global(|settings_store: &mut SettingsStore, cx| {
549        settings_store.set_user_settings(
550            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
551            cx,
552        )
553    })
554    .unwrap();
555
556    cx.run_until_parked();
557
558    server_cx.read(|cx| {
559        assert_eq!(
560            AllLanguageSettings::get_global(cx)
561                .language(None, Some(&"Rust".into()), cx)
562                .language_servers,
563            [ConfiguredLanguageServer::new("from-local-settings")],
564            "User language settings should be synchronized with the server settings"
565        )
566    });
567
568    server_cx
569        .update_global(|settings_store: &mut SettingsStore, cx| {
570            settings_store.set_server_settings(
571                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
572                cx,
573            )
574        })
575        .unwrap();
576
577    cx.run_until_parked();
578
579    server_cx.read(|cx| {
580        assert_eq!(
581            AllLanguageSettings::get_global(cx)
582                .language(None, Some(&"Rust".into()), cx)
583                .language_servers,
584            [ConfiguredLanguageServer::new("from-server-settings")],
585            "Server language settings should take precedence over the user settings"
586        )
587    });
588
589    fs.insert_tree(
590        "/code/project1/.zed",
591        json!({
592            "settings.json": r#"
593                  {
594                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
595                    "lsp": {
596                      "override-rust-analyzer": {
597                        "binary": {
598                          "path": "~/.cargo/bin/rust-analyzer"
599                        }
600                      }
601                    }
602                  }"#
603        }),
604    )
605    .await;
606
607    let worktree_id = project
608        .update(cx, |project, cx| {
609            project.languages().add(rust_lang());
610            project.find_or_create_worktree("/code/project1", true, cx)
611        })
612        .await
613        .unwrap()
614        .0
615        .read_with(cx, |worktree, _| worktree.id());
616
617    let buffer = project
618        .update(cx, |project, cx| {
619            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
620        })
621        .await
622        .unwrap();
623    cx.run_until_parked();
624
625    server_cx.read(|cx| {
626        let worktree_id = headless
627            .read(cx)
628            .worktree_store
629            .read(cx)
630            .worktrees()
631            .next()
632            .unwrap()
633            .read(cx)
634            .id();
635        assert_eq!(
636            AllLanguageSettings::get(
637                Some(SettingsLocation {
638                    worktree_id,
639                    path: rel_path("src/lib.rs")
640                }),
641                cx
642            )
643            .language(None, Some(&"Rust".into()), cx)
644            .language_servers,
645            [ConfiguredLanguageServer::new("override-rust-analyzer")]
646        )
647    });
648
649    cx.read(|cx| {
650        assert_eq!(
651            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
652            [ConfiguredLanguageServer::new("override-rust-analyzer")]
653        )
654    });
655}
656
657#[gpui::test]
658async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
659    let fs = FakeFs::new(server_cx.executor());
660    fs.insert_tree(
661        path!("/code"),
662        json!({
663            "project1": {
664                ".git": {},
665                "README.md": "# project 1",
666                "src": {
667                    "lib.rs": "fn one() -> usize { 1 }"
668                }
669            },
670        }),
671    )
672    .await;
673
674    let (project, headless) = init_test(&fs, cx, server_cx).await;
675
676    fs.insert_tree(
677        path!("/code/project1/.zed"),
678        json!({
679            "settings.json": r#"
680          {
681            "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
682            "lsp": {
683              "rust-analyzer": {
684                "binary": {
685                  "path": "~/.cargo/bin/rust-analyzer"
686                }
687              },
688              "fake-analyzer": {
689               "binary": {
690                "path": "~/.cargo/bin/rust-analyzer"
691               }
692              }
693            }
694          }"#
695        }),
696    )
697    .await;
698
699    cx.update_entity(&project, |project, _| {
700        project.languages().register_test_language(LanguageConfig {
701            name: "Rust".into(),
702            matcher: (LanguageMatcher {
703                path_suffixes: vec!["rs".into()],
704                ..Default::default()
705            })
706            .into(),
707            ..Default::default()
708        });
709        project.languages().register_fake_lsp_adapter(
710            "Rust",
711            FakeLspAdapter {
712                name: "rust-analyzer",
713                capabilities: lsp::ServerCapabilities {
714                    completion_provider: Some(lsp::CompletionOptions::default()),
715                    rename_provider: Some(lsp::OneOf::Left(true)),
716                    ..lsp::ServerCapabilities::default()
717                },
718                ..FakeLspAdapter::default()
719            },
720        );
721        project.languages().register_fake_lsp_adapter(
722            "Rust",
723            FakeLspAdapter {
724                name: "fake-analyzer",
725                capabilities: lsp::ServerCapabilities {
726                    completion_provider: Some(lsp::CompletionOptions::default()),
727                    rename_provider: Some(lsp::OneOf::Left(true)),
728                    ..lsp::ServerCapabilities::default()
729                },
730                ..FakeLspAdapter::default()
731            },
732        )
733    });
734
735    let mut fake_lsp = server_cx.update(|cx| {
736        headless.read(cx).languages.register_fake_lsp_server(
737            LanguageServerName("rust-analyzer".into()),
738            lsp::ServerCapabilities {
739                completion_provider: Some(lsp::CompletionOptions::default()),
740                rename_provider: Some(lsp::OneOf::Left(true)),
741                ..lsp::ServerCapabilities::default()
742            },
743            None,
744        )
745    });
746
747    let mut fake_second_lsp = server_cx.update(|cx| {
748        headless.read(cx).languages.register_fake_lsp_adapter(
749            "Rust",
750            FakeLspAdapter {
751                name: "fake-analyzer",
752                capabilities: lsp::ServerCapabilities {
753                    completion_provider: Some(lsp::CompletionOptions::default()),
754                    rename_provider: Some(lsp::OneOf::Left(true)),
755                    ..lsp::ServerCapabilities::default()
756                },
757                ..FakeLspAdapter::default()
758            },
759        );
760        headless.read(cx).languages.register_fake_lsp_server(
761            LanguageServerName("fake-analyzer".into()),
762            lsp::ServerCapabilities {
763                completion_provider: Some(lsp::CompletionOptions::default()),
764                rename_provider: Some(lsp::OneOf::Left(true)),
765                ..lsp::ServerCapabilities::default()
766            },
767            None,
768        )
769    });
770
771    cx.run_until_parked();
772
773    let worktree_id = project
774        .update(cx, |project, cx| {
775            project.languages().add(rust_lang());
776            project.find_or_create_worktree(path!("/code/project1"), true, cx)
777        })
778        .await
779        .unwrap()
780        .0
781        .read_with(cx, |worktree, _| worktree.id());
782
783    // Wait for the settings to synchronize
784    cx.run_until_parked();
785
786    let (buffer, _handle) = project
787        .update(cx, |project, cx| {
788            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
789        })
790        .await
791        .unwrap();
792    cx.run_until_parked();
793
794    let fake_lsp = fake_lsp.next().await.unwrap();
795    let fake_second_lsp = fake_second_lsp.next().await.unwrap();
796
797    cx.read(|cx| {
798        assert_eq!(
799            LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
800            [
801                ConfiguredLanguageServer::new("rust-analyzer"),
802                ConfiguredLanguageServer::new("fake-analyzer"),
803            ]
804        )
805    });
806
807    let buffer_id = cx.read(|cx| {
808        let buffer = buffer.read(cx);
809        assert_eq!(buffer.language().unwrap().name(), "Rust");
810        buffer.remote_id()
811    });
812
813    server_cx.read(|cx| {
814        let buffer = headless
815            .read(cx)
816            .buffer_store
817            .read(cx)
818            .get(buffer_id)
819            .unwrap();
820
821        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
822    });
823
824    server_cx.read(|cx| {
825        let lsp_store = headless.read(cx).lsp_store.read(cx);
826        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
827    });
828
829    fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
830        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
831            label: "boop".to_string(),
832            ..Default::default()
833        }])))
834    });
835
836    fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
837        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
838            label: "beep".to_string(),
839            ..Default::default()
840        }])))
841    });
842
843    let result = project
844        .update(cx, |project, cx| {
845            project.completions(
846                &buffer,
847                0,
848                CompletionContext {
849                    trigger_kind: CompletionTriggerKind::INVOKED,
850                    trigger_character: None,
851                },
852                cx,
853            )
854        })
855        .await
856        .unwrap();
857
858    assert_eq!(
859        result
860            .into_iter()
861            .flat_map(|response| response.completions)
862            .map(|c| c.label.text)
863            .collect::<Vec<_>>(),
864        vec!["boop".to_string(), "beep".to_string()]
865    );
866
867    fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
868        Ok(Some(lsp::WorkspaceEdit {
869            changes: Some(
870                [(
871                    lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
872                    vec![lsp::TextEdit::new(
873                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
874                        "two".to_string(),
875                    )],
876                )]
877                .into_iter()
878                .collect(),
879            ),
880            ..Default::default()
881        }))
882    });
883
884    project
885        .update(cx, |project, cx| {
886            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
887        })
888        .await
889        .unwrap();
890
891    cx.run_until_parked();
892    buffer.update(cx, |buffer, _| {
893        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
894    })
895}
896
897#[gpui::test]
898async fn test_remote_code_action_resolve(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
899    cx.update(|cx| {
900        let settings_store = SettingsStore::test(cx);
901        cx.set_global(settings_store);
902        theme_settings::init(theme::LoadThemes::JustBase, cx);
903        release_channel::init(semver::Version::new(0, 0, 0), cx);
904        editor::init(cx);
905    });
906
907    let fs = FakeFs::new(server_cx.executor());
908    fs.insert_tree(
909        path!("/code"),
910        json!({
911            "project1": {
912                ".git": {},
913                "src": {
914                    "lib.rs": "fn one() -> usize { 1 }"
915                }
916            },
917        }),
918    )
919    .await;
920
921    let (project, headless) = init_test(&fs, cx, server_cx).await;
922
923    let capabilities = lsp::ServerCapabilities {
924        code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
925            lsp::CodeActionOptions {
926                resolve_provider: Some(true),
927                ..lsp::CodeActionOptions::default()
928            },
929        )),
930        ..lsp::ServerCapabilities::default()
931    };
932
933    cx.update_entity(&project, |project, _| {
934        project.languages().register_test_language(LanguageConfig {
935            name: "Rust".into(),
936            matcher: (LanguageMatcher {
937                path_suffixes: vec!["rs".into()],
938                ..Default::default()
939            })
940            .into(),
941            ..Default::default()
942        });
943        project.languages().register_fake_lsp_adapter(
944            "Rust",
945            FakeLspAdapter {
946                name: "rust-analyzer",
947                capabilities: capabilities.clone(),
948                ..FakeLspAdapter::default()
949            },
950        );
951    });
952
953    server_cx.update(|cx| {
954        headless.read(cx).languages.register_fake_lsp_server(
955            LanguageServerName("rust-analyzer".into()),
956            capabilities,
957            Some(Box::new(|fake_lsp| {
958                fake_lsp.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
959                    |_, _| async move {
960                        Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
961                            lsp::CodeAction {
962                                title: "Use two".to_string(),
963                                data: Some(serde_json::json!({ "id": "action" })),
964                                ..lsp::CodeAction::default()
965                            },
966                        )]))
967                    },
968                );
969                fake_lsp.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
970                    |mut action, _| async move {
971                        action.edit = Some(lsp::WorkspaceEdit {
972                            changes: Some(
973                                [(
974                                    lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs"))
975                                        .unwrap(),
976                                    vec![lsp::TextEdit::new(
977                                        lsp::Range::new(
978                                            lsp::Position::new(0, 3),
979                                            lsp::Position::new(0, 6),
980                                        ),
981                                        "two".to_string(),
982                                    )],
983                                )]
984                                .into_iter()
985                                .collect(),
986                            ),
987                            ..lsp::WorkspaceEdit::default()
988                        });
989                        Ok(action)
990                    },
991                );
992            })),
993        );
994    });
995
996    cx.run_until_parked();
997
998    let worktree_id = project
999        .update(cx, |project, cx| {
1000            project.languages().add(rust_lang());
1001            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1002        })
1003        .await
1004        .unwrap()
1005        .0
1006        .read_with(cx, |worktree, _| worktree.id());
1007    cx.run_until_parked();
1008
1009    let (buffer, _handle) = project
1010        .update(cx, |project, cx| {
1011            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1012        })
1013        .await
1014        .unwrap();
1015    cx.run_until_parked();
1016
1017    let cx = cx.add_empty_window();
1018    let workspace = cx.new_window_entity(|window, cx| {
1019        workspace::Workspace::test_new(project.clone(), window, cx)
1020    });
1021    let editor = cx.new_window_entity(|window, cx| {
1022        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
1023    });
1024    workspace.update_in(cx, |workspace, window, cx| {
1025        workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1026    });
1027    cx.run_until_parked();
1028
1029    editor.update_in(cx, |editor, window, cx| {
1030        editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1031            s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
1032        });
1033    });
1034    cx.executor()
1035        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
1036    cx.run_until_parked();
1037
1038    editor.update_in(cx, |editor, window, cx| {
1039        editor.toggle_code_actions(
1040            &ToggleCodeActions {
1041                deployed_from: None,
1042                quick_launch: false,
1043            },
1044            window,
1045            cx,
1046        );
1047    });
1048    cx.run_until_parked();
1049
1050    editor.update(cx, |editor, _| assert!(editor.context_menu_visible()));
1051
1052    let confirm_action = editor
1053        .update_in(cx, |editor, window, cx| {
1054            Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
1055        })
1056        .unwrap();
1057    confirm_action.await.unwrap();
1058
1059    buffer.read_with(cx, |buffer, _| {
1060        assert_eq!(buffer.text(), "fn two() -> usize { 1 }");
1061    });
1062}
1063
1064#[gpui::test]
1065async fn test_remote_code_lens_fetch_after_lsp_starts(
1066    cx: &mut TestAppContext,
1067    server_cx: &mut TestAppContext,
1068) {
1069    let fs = FakeFs::new(server_cx.executor());
1070    fs.insert_tree(
1071        path!("/code"),
1072        json!({
1073            "project1": {
1074                ".git": {},
1075                "src": {
1076                    "lib.rs": "fn one() -> usize { 1 }"
1077                }
1078            },
1079        }),
1080    )
1081    .await;
1082
1083    let (project, headless) = init_test(&fs, cx, server_cx).await;
1084
1085    let capabilities = lsp::ServerCapabilities {
1086        code_lens_provider: Some(lsp::CodeLensOptions {
1087            resolve_provider: None,
1088        }),
1089        ..lsp::ServerCapabilities::default()
1090    };
1091
1092    cx.update_entity(&project, |project, _| {
1093        project.languages().register_test_language(LanguageConfig {
1094            name: "Rust".into(),
1095            matcher: (LanguageMatcher {
1096                path_suffixes: vec!["rs".into()],
1097                ..Default::default()
1098            })
1099            .into(),
1100            ..Default::default()
1101        });
1102        project.languages().register_fake_lsp_adapter(
1103            "Rust",
1104            FakeLspAdapter {
1105                name: "rust-analyzer",
1106                capabilities: capabilities.clone(),
1107                ..FakeLspAdapter::default()
1108            },
1109        );
1110    });
1111
1112    cx.run_until_parked();
1113
1114    let worktree_id = project
1115        .update(cx, |project, cx| {
1116            project.languages().add(rust_lang());
1117            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1118        })
1119        .await
1120        .unwrap()
1121        .0
1122        .read_with(cx, |worktree, _| worktree.id());
1123    cx.run_until_parked();
1124
1125    let (buffer, _handle) = project
1126        .update(cx, |project, cx| {
1127            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1128        })
1129        .await
1130        .unwrap();
1131    cx.run_until_parked();
1132
1133    // Prime the code lens cache while no LSP server exists on the host.
1134    // This simulates the race where the editor fetches code lenses during
1135    // initial paint before the language server has started.
1136    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1137    let initial_actions = lsp_store
1138        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1139        .await
1140        .unwrap();
1141    assert_eq!(
1142        initial_actions.map(|a| a.len()),
1143        Some(0),
1144        "Before any LSP starts, code lenses should be empty"
1145    );
1146
1147    // Now register the LSP on the host. This triggers server startup and
1148    // buffer registration, propagating capabilities to the client.
1149    server_cx.update(|cx| {
1150        headless.read(cx).languages.register_fake_lsp_server(
1151            LanguageServerName("rust-analyzer".into()),
1152            capabilities,
1153            Some(Box::new(|fake_lsp| {
1154                fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1155                    |_, _| async move {
1156                        Ok(Some(vec![lsp::CodeLens {
1157                            range: lsp::Range::new(
1158                                lsp::Position::new(0, 0),
1159                                lsp::Position::new(0, 9),
1160                            ),
1161                            command: Some(lsp::Command {
1162                                title: "1 reference".to_string(),
1163                                command: "lens_cmd".to_string(),
1164                                arguments: None,
1165                            }),
1166                            data: None,
1167                        }]))
1168                    },
1169                );
1170            })),
1171        );
1172    });
1173
1174    // Trigger re-evaluation of language servers for the already-open buffer.
1175    server_cx.update_entity(&headless, |headless, cx| {
1176        headless.lsp_store.update(cx, |lsp_store, cx| {
1177            lsp_store.restart_all_language_servers(cx);
1178        });
1179    });
1180    cx.run_until_parked();
1181
1182    // A subsequent fetch must bypass the stale (empty) cache now that a
1183    // new server is available.
1184    let actions = lsp_store
1185        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1186        .await
1187        .unwrap();
1188    let actions = actions.expect("Should have code lens actions after LSP starts");
1189    assert_eq!(
1190        actions.len(),
1191        1,
1192        "Should have fetched one code lens from the newly started LSP"
1193    );
1194    assert_eq!(
1195        actions.values().next().unwrap().lsp_action.title(),
1196        "1 reference",
1197    );
1198}
1199
1200#[gpui::test]
1201async fn test_remote_code_lens_refetch_after_refresh(
1202    cx: &mut TestAppContext,
1203    server_cx: &mut TestAppContext,
1204) {
1205    let fs = FakeFs::new(server_cx.executor());
1206    fs.insert_tree(
1207        path!("/code"),
1208        json!({
1209            "project1": {
1210                ".git": {},
1211                "src": {
1212                    "lib.rs": "fn one() -> usize { 1 }"
1213                }
1214            },
1215        }),
1216    )
1217    .await;
1218
1219    let (project, headless) = init_test(&fs, cx, server_cx).await;
1220
1221    let capabilities = lsp::ServerCapabilities {
1222        code_lens_provider: Some(lsp::CodeLensOptions {
1223            resolve_provider: None,
1224        }),
1225        ..lsp::ServerCapabilities::default()
1226    };
1227
1228    cx.update_entity(&project, |project, _| {
1229        project.languages().register_test_language(LanguageConfig {
1230            name: "Rust".into(),
1231            matcher: Arc::new(LanguageMatcher {
1232                path_suffixes: vec!["rs".into()],
1233                ..LanguageMatcher::default()
1234            }),
1235            ..LanguageConfig::default()
1236        });
1237        project.languages().register_fake_lsp_adapter(
1238            "Rust",
1239            FakeLspAdapter {
1240                name: "rust-analyzer",
1241                capabilities: capabilities.clone(),
1242                ..FakeLspAdapter::default()
1243            },
1244        );
1245    });
1246
1247    let lens_generation = Arc::new(AtomicUsize::new(0));
1248    let mut fake_lsp_servers = server_cx.update(|cx| {
1249        headless.read(cx).languages.register_fake_lsp_server(
1250            LanguageServerName("rust-analyzer".into()),
1251            capabilities,
1252            Some(Box::new({
1253                let lens_generation = lens_generation.clone();
1254                move |fake_lsp| {
1255                    let lens_generation = lens_generation.clone();
1256                    fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1257                        move |_, _| {
1258                            let generation = lens_generation.load(Ordering::Acquire);
1259                            async move {
1260                                Ok(Some(vec![lsp::CodeLens {
1261                                    range: lsp::Range::new(
1262                                        lsp::Position::new(0, 0),
1263                                        lsp::Position::new(0, 9),
1264                                    ),
1265                                    command: Some(lsp::Command {
1266                                        title: format!("{generation} references"),
1267                                        command: "lens_cmd".to_string(),
1268                                        arguments: None,
1269                                    }),
1270                                    data: None,
1271                                }]))
1272                            }
1273                        },
1274                    );
1275                }
1276            })),
1277        )
1278    });
1279
1280    cx.run_until_parked();
1281
1282    let worktree_id = project
1283        .update(cx, |project, cx| {
1284            project.languages().add(rust_lang());
1285            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1286        })
1287        .await
1288        .unwrap()
1289        .0
1290        .read_with(cx, |worktree, _| worktree.id());
1291    cx.run_until_parked();
1292
1293    let (buffer, _handle) = project
1294        .update(cx, |project, cx| {
1295            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1296        })
1297        .await
1298        .unwrap();
1299    let fake_lsp = fake_lsp_servers.next().await.unwrap();
1300    cx.run_until_parked();
1301
1302    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1303    let actions = lsp_store
1304        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1305        .await
1306        .unwrap()
1307        .expect("Should have code lens actions after the initial fetch");
1308    assert_eq!(
1309        actions
1310            .values()
1311            .map(|action| action.lsp_action.title().to_owned())
1312            .collect::<Vec<_>>(),
1313        vec!["0 references".to_string()],
1314        "Expected the initial code lens data on the client",
1315    );
1316
1317    lens_generation.store(1, Ordering::Release);
1318    fake_lsp
1319        .request::<lsp::request::CodeLensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1320        .await
1321        .into_response()
1322        .unwrap();
1323    cx.run_until_parked();
1324
1325    let actions = lsp_store
1326        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1327        .await
1328        .unwrap()
1329        .expect("Should have code lens actions after the refresh");
1330    assert_eq!(
1331        actions
1332            .values()
1333            .map(|action| action.lsp_action.title().to_owned())
1334            .collect::<Vec<_>>(),
1335        vec!["1 references".to_string()],
1336        "Expected the client to re-fetch code lens data after the server-initiated refresh, without any buffer edits",
1337    );
1338}
1339
1340#[gpui::test]
1341async fn test_remote_lsp_data_cache_converges_with_multiple_servers(
1342    cx: &mut TestAppContext,
1343    server_cx: &mut TestAppContext,
1344) {
1345    let fs = FakeFs::new(server_cx.executor());
1346    fs.insert_tree(
1347        path!("/code"),
1348        json!({
1349            "project1": {
1350                ".git": {},
1351                "src": {
1352                    "lib.rs": "fn one() -> usize { 1 }"
1353                }
1354            },
1355            "project2": {
1356                ".git": {},
1357                "src": {
1358                    "lib.rs": "fn two() -> usize { 2 }"
1359                }
1360            },
1361        }),
1362    )
1363    .await;
1364
1365    let (project, headless) = init_test(&fs, cx, server_cx).await;
1366
1367    let capabilities = lsp::ServerCapabilities {
1368        code_lens_provider: Some(lsp::CodeLensOptions {
1369            resolve_provider: None,
1370        }),
1371        ..lsp::ServerCapabilities::default()
1372    };
1373
1374    cx.update_entity(&project, |project, _| {
1375        project.languages().register_test_language(LanguageConfig {
1376            name: "Rust".into(),
1377            matcher: Arc::new(LanguageMatcher {
1378                path_suffixes: vec!["rs".into()],
1379                ..LanguageMatcher::default()
1380            }),
1381            ..LanguageConfig::default()
1382        });
1383        project.languages().register_fake_lsp_adapter(
1384            "Rust",
1385            FakeLspAdapter {
1386                name: "rust-analyzer",
1387                capabilities: capabilities.clone(),
1388                ..FakeLspAdapter::default()
1389            },
1390        );
1391    });
1392
1393    let queried_uris = Arc::new(std::sync::Mutex::new(Vec::new()));
1394    let mut fake_lsp_servers = server_cx.update(|cx| {
1395        headless.read(cx).languages.register_fake_lsp_server(
1396            LanguageServerName("rust-analyzer".into()),
1397            capabilities,
1398            Some(Box::new({
1399                let queried_uris = queried_uris.clone();
1400                move |fake_lsp| {
1401                    let queried_uris = queried_uris.clone();
1402                    fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1403                        move |params, _| {
1404                            queried_uris.lock().unwrap().push(params.text_document.uri);
1405                            async move {
1406                                Ok(Some(vec![lsp::CodeLens {
1407                                    range: lsp::Range::new(
1408                                        lsp::Position::new(0, 0),
1409                                        lsp::Position::new(0, 9),
1410                                    ),
1411                                    command: Some(lsp::Command {
1412                                        title: "the lens".to_string(),
1413                                        command: "lens_cmd".to_string(),
1414                                        arguments: None,
1415                                    }),
1416                                    data: None,
1417                                }]))
1418                            }
1419                        },
1420                    );
1421                }
1422            })),
1423        )
1424    });
1425
1426    cx.run_until_parked();
1427
1428    project.update(cx, |project, cx| {
1429        project.languages().add(rust_lang());
1430        cx.notify();
1431    });
1432    let mut buffers = Vec::new();
1433    let mut fake_lsps = Vec::new();
1434    for project_root in [path!("/code/project1"), path!("/code/project2")] {
1435        let worktree_id = project
1436            .update(cx, |project, cx| {
1437                project.find_or_create_worktree(project_root, true, cx)
1438            })
1439            .await
1440            .unwrap()
1441            .0
1442            .read_with(cx, |worktree, _| worktree.id());
1443        cx.run_until_parked();
1444        buffers.push(
1445            project
1446                .update(cx, |project, cx| {
1447                    project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1448                })
1449                .await
1450                .unwrap(),
1451        );
1452        fake_lsps.push(fake_lsp_servers.next().await.unwrap());
1453        cx.run_until_parked();
1454    }
1455    let (buffer_1, _handle_1) = &buffers[0];
1456    let queried_lib_1 = vec![lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap()];
1457
1458    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1459    lsp_store
1460        .update(cx, |lsp_store, cx| {
1461            lsp_store.code_lens_actions(buffer_1, cx)
1462        })
1463        .await
1464        .unwrap();
1465    assert_eq!(
1466        *queried_uris.lock().unwrap(),
1467        queried_lib_1,
1468        "Expected the initial fetch to query the buffer's own language server only",
1469    );
1470
1471    lsp_store
1472        .update(cx, |lsp_store, cx| {
1473            lsp_store.code_lens_actions(buffer_1, cx)
1474        })
1475        .await
1476        .unwrap();
1477    assert_eq!(
1478        *queried_uris.lock().unwrap(),
1479        queried_lib_1,
1480        "Expected a repeated fetch to be served from the cache, despite the other worktree's server \
1481        never answering for this buffer",
1482    );
1483
1484    let (buffer_1, _) = &buffers[0];
1485    buffer_1.update(cx, |buffer, cx| buffer.set_language(None, cx));
1486    cx.run_until_parked();
1487    let actions = lsp_store
1488        .update(cx, |lsp_store, cx| {
1489            lsp_store.code_lens_actions(buffer_1, cx)
1490        })
1491        .await
1492        .unwrap()
1493        .expect("Should have code lens actions cached");
1494    assert_eq!(
1495        actions
1496            .values()
1497            .map(|action| action.lsp_action.title().to_owned())
1498            .collect::<Vec<_>>(),
1499        vec!["the lens".to_string()],
1500        "Expected the cached data to survive an empty relevant server set (buffer without a language)",
1501    );
1502    assert_eq!(
1503        *queried_uris.lock().unwrap(),
1504        queried_lib_1,
1505        "Expected no new queries for the buffer without a language",
1506    );
1507}
1508
1509#[gpui::test]
1510async fn test_remote_per_server_refresh_queries_only_the_refreshed_server(
1511    cx: &mut TestAppContext,
1512    server_cx: &mut TestAppContext,
1513) {
1514    let fs = FakeFs::new(server_cx.executor());
1515    fs.insert_tree(
1516        path!("/code"),
1517        json!({
1518            "project1": {
1519                ".git": {},
1520                "src": {
1521                    "lib.fake": "fn one() -> usize { 1 }"
1522                }
1523            },
1524        }),
1525    )
1526    .await;
1527
1528    let (project, headless) = init_test(&fs, cx, server_cx).await;
1529
1530    let capabilities = lsp::ServerCapabilities {
1531        code_lens_provider: Some(lsp::CodeLensOptions {
1532            resolve_provider: None,
1533        }),
1534        ..lsp::ServerCapabilities::default()
1535    };
1536
1537    // Both the client and the headless registries need the language and the adapters:
1538    // the client filters relevant servers by adapter names, while the headless one
1539    // detects the buffer's language and starts the servers.
1540    let fake_language_config = || LanguageConfig {
1541        name: "Fake".into(),
1542        matcher: Arc::new(LanguageMatcher {
1543            path_suffixes: vec!["fake".into()],
1544            ..LanguageMatcher::default()
1545        }),
1546        ..LanguageConfig::default()
1547    };
1548    let fake_adapter =
1549        |server_name: &'static str, capabilities: &lsp::ServerCapabilities| FakeLspAdapter {
1550            name: server_name,
1551            capabilities: capabilities.clone(),
1552            ..FakeLspAdapter::default()
1553        };
1554    cx.update_entity(&project, |project, _| {
1555        project
1556            .languages()
1557            .register_test_language(fake_language_config());
1558        for server_name in ["server-a", "server-b"] {
1559            project
1560                .languages()
1561                .register_fake_lsp_adapter("Fake", fake_adapter(server_name, &capabilities));
1562        }
1563    });
1564
1565    let request_counter = |fake_lsp: &mut lsp::FakeLanguageServer, counter: &Arc<AtomicUsize>| {
1566        let counter = counter.clone();
1567        fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
1568            counter.fetch_add(1, Ordering::Release);
1569            async move { Ok(Some(Vec::new())) }
1570        });
1571    };
1572    let lens_requests_a = Arc::new(AtomicUsize::new(0));
1573    let lens_requests_b = Arc::new(AtomicUsize::new(0));
1574    let (mut fake_lsp_servers_a, mut fake_lsp_servers_b) = server_cx.update(|cx| {
1575        let languages = &headless.read(cx).languages;
1576        languages.register_test_language(fake_language_config());
1577        for server_name in ["server-a", "server-b"] {
1578            languages.register_fake_lsp_adapter("Fake", fake_adapter(server_name, &capabilities));
1579        }
1580        let fake_lsp_servers_a = languages.register_fake_lsp_server(
1581            LanguageServerName("server-a".into()),
1582            capabilities.clone(),
1583            Some(Box::new({
1584                let lens_requests_a = lens_requests_a.clone();
1585                move |fake_lsp| request_counter(fake_lsp, &lens_requests_a)
1586            })),
1587        );
1588        let fake_lsp_servers_b = languages.register_fake_lsp_server(
1589            LanguageServerName("server-b".into()),
1590            capabilities.clone(),
1591            Some(Box::new({
1592                let lens_requests_b = lens_requests_b.clone();
1593                move |fake_lsp| request_counter(fake_lsp, &lens_requests_b)
1594            })),
1595        );
1596        (fake_lsp_servers_a, fake_lsp_servers_b)
1597    });
1598
1599    cx.run_until_parked();
1600
1601    let worktree_id = project
1602        .update(cx, |project, cx| {
1603            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1604        })
1605        .await
1606        .unwrap()
1607        .0
1608        .read_with(cx, |worktree, _| worktree.id());
1609    cx.run_until_parked();
1610
1611    let (buffer, _handle) = project
1612        .update(cx, |project, cx| {
1613            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.fake")), cx)
1614        })
1615        .await
1616        .unwrap();
1617    let fake_lsp_a = fake_lsp_servers_a.next().await.unwrap();
1618    let _fake_lsp_b = fake_lsp_servers_b.next().await.unwrap();
1619    cx.run_until_parked();
1620
1621    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1622    lsp_store
1623        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1624        .await
1625        .unwrap();
1626    assert_eq!(
1627        (
1628            lens_requests_a.load(Ordering::Acquire),
1629            lens_requests_b.load(Ordering::Acquire),
1630        ),
1631        (1, 1),
1632        "Expected the initial fetch to query both servers",
1633    );
1634
1635    fake_lsp_a
1636        .request::<lsp::request::CodeLensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1637        .await
1638        .into_response()
1639        .unwrap();
1640    cx.run_until_parked();
1641
1642    lsp_store
1643        .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1644        .await
1645        .unwrap();
1646    assert_eq!(
1647        (
1648            lens_requests_a.load(Ordering::Acquire),
1649            lens_requests_b.load(Ordering::Acquire),
1650        ),
1651        (2, 1),
1652        "Expected the refetch after a per-server refresh to query the refreshed server only",
1653    );
1654}
1655
1656#[gpui::test]
1657async fn test_remote_code_lens_resolve(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1658    cx.update(|cx| {
1659        let settings_store = SettingsStore::test(cx);
1660        cx.set_global(settings_store);
1661        theme_settings::init(theme::LoadThemes::JustBase, cx);
1662        release_channel::init(semver::Version::new(0, 0, 0), cx);
1663        editor::init(cx);
1664        SettingsStore::update_global(cx, |store, cx| {
1665            store.update_user_settings(cx, |settings| {
1666                settings.editor.code_lens = Some(settings::CodeLens::Menu);
1667            });
1668        });
1669    });
1670
1671    let fs = FakeFs::new(server_cx.executor());
1672    fs.insert_tree(
1673        path!("/code"),
1674        json!({
1675            "project1": {
1676                ".git": {},
1677                "src": {
1678                    "lib.rs": "fn one() -> usize { 1 }"
1679                }
1680            },
1681        }),
1682    )
1683    .await;
1684
1685    let (project, headless) = init_test(&fs, cx, server_cx).await;
1686
1687    let capabilities = lsp::ServerCapabilities {
1688        code_lens_provider: Some(lsp::CodeLensOptions {
1689            resolve_provider: Some(true),
1690        }),
1691        ..lsp::ServerCapabilities::default()
1692    };
1693
1694    cx.update_entity(&project, |project, _| {
1695        project.languages().register_test_language(LanguageConfig {
1696            name: "Rust".into(),
1697            matcher: (LanguageMatcher {
1698                path_suffixes: vec!["rs".into()],
1699                ..Default::default()
1700            })
1701            .into(),
1702            ..Default::default()
1703        });
1704        project.languages().register_fake_lsp_adapter(
1705            "Rust",
1706            FakeLspAdapter {
1707                name: "rust-analyzer",
1708                capabilities: capabilities.clone(),
1709                ..FakeLspAdapter::default()
1710            },
1711        );
1712    });
1713
1714    server_cx.update(|cx| {
1715        headless.read(cx).languages.register_fake_lsp_server(
1716            LanguageServerName("rust-analyzer".into()),
1717            capabilities,
1718            Some(Box::new(|fake_lsp| {
1719                fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1720                    |_, _| async move {
1721                        Ok(Some(vec![lsp::CodeLens {
1722                            range: lsp::Range::new(
1723                                lsp::Position::new(0, 0),
1724                                lsp::Position::new(0, 9),
1725                            ),
1726                            command: None,
1727                            data: Some(serde_json::json!({ "id": "lens" })),
1728                        }]))
1729                    },
1730                );
1731                fake_lsp.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
1732                    |lens, _| async move {
1733                        Ok(lsp::CodeLens {
1734                            command: Some(lsp::Command {
1735                                title: "1 reference".to_string(),
1736                                command: "noop".to_string(),
1737                                arguments: None,
1738                            }),
1739                            ..lens
1740                        })
1741                    },
1742                );
1743            })),
1744        );
1745    });
1746
1747    cx.run_until_parked();
1748
1749    let worktree_id = project
1750        .update(cx, |project, cx| {
1751            project.languages().add(rust_lang());
1752            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1753        })
1754        .await
1755        .unwrap()
1756        .0
1757        .read_with(cx, |worktree, _| worktree.id());
1758    cx.run_until_parked();
1759
1760    let (buffer, _handle) = project
1761        .update(cx, |project, cx| {
1762            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1763        })
1764        .await
1765        .unwrap();
1766    cx.run_until_parked();
1767
1768    let cx = cx.add_empty_window();
1769    let workspace = cx.new_window_entity(|window, cx| {
1770        workspace::Workspace::test_new(project.clone(), window, cx)
1771    });
1772    let editor = cx.new_window_entity(|window, cx| {
1773        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
1774    });
1775    workspace.update_in(cx, |workspace, window, cx| {
1776        workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1777    });
1778
1779    editor.update_in(cx, |editor, window, cx| {
1780        editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1781            s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
1782        });
1783    });
1784    cx.executor()
1785        .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
1786    cx.run_until_parked();
1787
1788    editor.update_in(cx, |editor, window, cx| {
1789        editor.toggle_code_actions(
1790            &ToggleCodeActions {
1791                deployed_from: None,
1792                quick_launch: false,
1793            },
1794            window,
1795            cx,
1796        );
1797    });
1798    cx.run_until_parked();
1799
1800    editor.update(cx, |editor, _| {
1801        assert!(editor.context_menu_visible());
1802        let menu = editor.context_menu().borrow();
1803        let actions_menu = match menu.as_ref() {
1804            Some(CodeContextMenu::CodeActions(m)) => m,
1805            _ => panic!("Expected code actions menu to be visible"),
1806        };
1807        let item = actions_menu
1808            .actions
1809            .get(0)
1810            .expect("Expected at least one item in menu");
1811        assert_eq!(item.label(), "1 reference");
1812    });
1813}
1814
1815#[gpui::test]
1816async fn test_remote_cancel_language_server_work(
1817    cx: &mut TestAppContext,
1818    server_cx: &mut TestAppContext,
1819) {
1820    let fs = FakeFs::new(server_cx.executor());
1821    fs.insert_tree(
1822        path!("/code"),
1823        json!({
1824            "project1": {
1825                ".git": {},
1826                "README.md": "# project 1",
1827                "src": {
1828                    "lib.rs": "fn one() -> usize { 1 }"
1829                }
1830            },
1831        }),
1832    )
1833    .await;
1834
1835    let (project, headless) = init_test(&fs, cx, server_cx).await;
1836
1837    fs.insert_tree(
1838        path!("/code/project1/.zed"),
1839        json!({
1840            "settings.json": r#"
1841          {
1842            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
1843            "lsp": {
1844              "rust-analyzer": {
1845                "binary": {
1846                  "path": "~/.cargo/bin/rust-analyzer"
1847                }
1848              }
1849            }
1850          }"#
1851        }),
1852    )
1853    .await;
1854
1855    cx.update_entity(&project, |project, _| {
1856        project.languages().register_test_language(LanguageConfig {
1857            name: "Rust".into(),
1858            matcher: (LanguageMatcher {
1859                path_suffixes: vec!["rs".into()],
1860                ..Default::default()
1861            })
1862            .into(),
1863            ..Default::default()
1864        });
1865        project.languages().register_fake_lsp_adapter(
1866            "Rust",
1867            FakeLspAdapter {
1868                name: "rust-analyzer",
1869                ..Default::default()
1870            },
1871        )
1872    });
1873
1874    let mut fake_lsp = server_cx.update(|cx| {
1875        headless.read(cx).languages.register_fake_lsp_server(
1876            LanguageServerName("rust-analyzer".into()),
1877            Default::default(),
1878            None,
1879        )
1880    });
1881
1882    cx.run_until_parked();
1883
1884    let worktree_id = project
1885        .update(cx, |project, cx| {
1886            project.find_or_create_worktree(path!("/code/project1"), true, cx)
1887        })
1888        .await
1889        .unwrap()
1890        .0
1891        .read_with(cx, |worktree, _| worktree.id());
1892
1893    cx.run_until_parked();
1894
1895    let (buffer, _handle) = project
1896        .update(cx, |project, cx| {
1897            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1898        })
1899        .await
1900        .unwrap();
1901
1902    cx.run_until_parked();
1903
1904    let mut fake_lsp = fake_lsp.next().await.unwrap();
1905
1906    // Cancelling all language server work for a given buffer
1907    {
1908        // Two operations, one cancellable and one not.
1909        fake_lsp
1910            .start_progress_with(
1911                "another-token",
1912                lsp::WorkDoneProgressBegin {
1913                    cancellable: Some(false),
1914                    ..Default::default()
1915                },
1916                DEFAULT_LSP_REQUEST_TIMEOUT,
1917            )
1918            .await;
1919
1920        let progress_token = "the-progress-token";
1921        fake_lsp
1922            .start_progress_with(
1923                progress_token,
1924                lsp::WorkDoneProgressBegin {
1925                    cancellable: Some(true),
1926                    ..Default::default()
1927                },
1928                DEFAULT_LSP_REQUEST_TIMEOUT,
1929            )
1930            .await;
1931
1932        cx.executor().run_until_parked();
1933
1934        project.update(cx, |project, cx| {
1935            project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
1936        });
1937
1938        cx.executor().run_until_parked();
1939
1940        // Verify the cancellation was received on the server side
1941        let cancel_notification = fake_lsp
1942            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1943            .await;
1944        assert_eq!(
1945            cancel_notification.token,
1946            lsp::NumberOrString::String(progress_token.into())
1947        );
1948    }
1949
1950    // Cancelling work by server_id and token
1951    {
1952        let server_id = fake_lsp.server.server_id();
1953        let progress_token = "the-progress-token";
1954
1955        fake_lsp
1956            .start_progress_with(
1957                progress_token,
1958                lsp::WorkDoneProgressBegin {
1959                    cancellable: Some(true),
1960                    ..Default::default()
1961                },
1962                DEFAULT_LSP_REQUEST_TIMEOUT,
1963            )
1964            .await;
1965
1966        cx.executor().run_until_parked();
1967
1968        project.update(cx, |project, cx| {
1969            project.cancel_language_server_work(
1970                server_id,
1971                Some(ProgressToken::String(SharedString::from(progress_token))),
1972                cx,
1973            )
1974        });
1975
1976        cx.executor().run_until_parked();
1977
1978        // Verify the cancellation was received on the server side
1979        let cancel_notification = fake_lsp
1980            .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1981            .await;
1982        assert_eq!(
1983            cancel_notification.token,
1984            lsp::NumberOrString::String(progress_token.to_owned())
1985        );
1986    }
1987}
1988
1989#[gpui::test]
1990async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1991    let fs = FakeFs::new(server_cx.executor());
1992    fs.insert_tree(
1993        path!("/code"),
1994        json!({
1995            "project1": {
1996                ".git": {},
1997                "README.md": "# project 1",
1998                "src": {
1999                    "lib.rs": "fn one() -> usize { 1 }"
2000                }
2001            },
2002        }),
2003    )
2004    .await;
2005
2006    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2007    let (worktree, _) = project
2008        .update(cx, |project, cx| {
2009            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2010        })
2011        .await
2012        .unwrap();
2013
2014    let worktree_id = cx.update(|cx| worktree.read(cx).id());
2015
2016    let buffer = project
2017        .update(cx, |project, cx| {
2018            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2019        })
2020        .await
2021        .unwrap();
2022
2023    fs.save(
2024        &PathBuf::from(path!("/code/project1/src/lib.rs")),
2025        &("bangles".to_string().into()),
2026        LineEnding::Unix,
2027    )
2028    .await
2029    .unwrap();
2030
2031    cx.run_until_parked();
2032
2033    buffer.update(cx, |buffer, cx| {
2034        assert_eq!(buffer.text(), "bangles");
2035        buffer.edit([(0..0, "a")], None, cx);
2036    });
2037
2038    fs.save(
2039        &PathBuf::from(path!("/code/project1/src/lib.rs")),
2040        &("bloop".to_string().into()),
2041        LineEnding::Unix,
2042    )
2043    .await
2044    .unwrap();
2045
2046    cx.run_until_parked();
2047    cx.update(|cx| {
2048        assert!(buffer.read(cx).has_conflict());
2049    });
2050
2051    project
2052        .update(cx, |project, cx| {
2053            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
2054        })
2055        .await
2056        .unwrap();
2057    cx.run_until_parked();
2058
2059    cx.update(|cx| {
2060        assert!(!buffer.read(cx).has_conflict());
2061    });
2062}
2063
2064#[gpui::test]
2065async fn test_remote_resolve_path_in_buffer(
2066    cx: &mut TestAppContext,
2067    server_cx: &mut TestAppContext,
2068) {
2069    let fs = FakeFs::new(server_cx.executor());
2070    // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
2071    fs.insert_tree(
2072        path!("/code"),
2073        json!({
2074            "project1": {
2075                ".git": {},
2076                "README.md": "# project 1",
2077                "src": {
2078                    "lib.rs": "fn one() -> usize { 1 }"
2079                }
2080            },
2081            "project2": {
2082                ".git": {},
2083                "README.md": "# project 2",
2084                "src": {
2085                    "lib.rs": "fn two() -> usize { 2 }"
2086                }
2087            }
2088        }),
2089    )
2090    .await;
2091
2092    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2093
2094    let _ = project
2095        .update(cx, |project, cx| {
2096            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2097        })
2098        .await
2099        .unwrap();
2100
2101    let (worktree2, _) = project
2102        .update(cx, |project, cx| {
2103            project.find_or_create_worktree(path!("/code/project2"), true, cx)
2104        })
2105        .await
2106        .unwrap();
2107
2108    let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
2109
2110    cx.run_until_parked();
2111
2112    let buffer2 = project
2113        .update(cx, |project, cx| {
2114            project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
2115        })
2116        .await
2117        .unwrap();
2118
2119    let path = project
2120        .update(cx, |project, cx| {
2121            project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
2122        })
2123        .await
2124        .unwrap();
2125    assert!(path.is_file());
2126    assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
2127
2128    let path = project
2129        .update(cx, |project, cx| {
2130            project.resolve_path_in_buffer("../README.md", &buffer2, cx)
2131        })
2132        .await
2133        .unwrap();
2134    assert!(path.is_file());
2135    assert_eq!(
2136        path.project_path().unwrap().clone(),
2137        (worktree2_id, rel_path("README.md")).into()
2138    );
2139
2140    let path = project
2141        .update(cx, |project, cx| {
2142            project.resolve_path_in_buffer("../src", &buffer2, cx)
2143        })
2144        .await
2145        .unwrap();
2146    assert_eq!(
2147        path.project_path().unwrap().clone(),
2148        (worktree2_id, rel_path("src")).into()
2149    );
2150    assert!(path.is_dir());
2151}
2152
2153#[gpui::test]
2154async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2155    let fs = FakeFs::new(server_cx.executor());
2156    fs.insert_tree(
2157        path!("/code"),
2158        json!({
2159            "project1": {
2160                ".git": {},
2161                "README.md": "# project 1",
2162                "src": {
2163                    "lib.rs": "fn one() -> usize { 1 }"
2164                }
2165            },
2166        }),
2167    )
2168    .await;
2169
2170    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2171
2172    let path = project
2173        .update(cx, |project, cx| {
2174            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
2175        })
2176        .await
2177        .unwrap();
2178
2179    assert!(path.is_file());
2180    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
2181
2182    let path = project
2183        .update(cx, |project, cx| {
2184            project.resolve_abs_path(path!("/code/project1/src"), cx)
2185        })
2186        .await
2187        .unwrap();
2188
2189    assert!(path.is_dir());
2190    assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
2191
2192    let path = project
2193        .update(cx, |project, cx| {
2194            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
2195        })
2196        .await;
2197    assert!(path.is_none());
2198}
2199
2200#[gpui::test(iterations = 10)]
2201async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2202    let fs = FakeFs::new(server_cx.executor());
2203    fs.insert_tree(
2204        "/code",
2205        json!({
2206            "project1": {
2207                ".git": {},
2208                "README.md": "# project 1",
2209                "src": {
2210                    "lib.rs": "fn one() -> usize { 1 }"
2211                }
2212            },
2213        }),
2214    )
2215    .await;
2216
2217    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2218    let (worktree, _) = project
2219        .update(cx, |project, cx| {
2220            project.find_or_create_worktree("/code/project1", true, cx)
2221        })
2222        .await
2223        .unwrap();
2224    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
2225
2226    // Open a buffer on the client but cancel after a random amount of time.
2227    let buffer = project.update(cx, |p, cx| {
2228        p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2229    });
2230    cx.executor().simulate_random_delay().await;
2231    drop(buffer);
2232
2233    // Try opening the same buffer again as the client, and ensure we can
2234    // still do it despite the cancellation above.
2235    let buffer = project
2236        .update(cx, |p, cx| {
2237            p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2238        })
2239        .await
2240        .unwrap();
2241
2242    buffer.read_with(cx, |buf, _| {
2243        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
2244    });
2245}
2246
2247#[gpui::test]
2248async fn test_adding_then_removing_then_adding_worktrees(
2249    cx: &mut TestAppContext,
2250    server_cx: &mut TestAppContext,
2251) {
2252    let fs = FakeFs::new(server_cx.executor());
2253    fs.insert_tree(
2254        path!("/code"),
2255        json!({
2256            "project1": {
2257                ".git": {},
2258                "README.md": "# project 1",
2259                "src": {
2260                    "lib.rs": "fn one() -> usize { 1 }"
2261                }
2262            },
2263            "project2": {
2264                "README.md": "# project 2",
2265            },
2266        }),
2267    )
2268    .await;
2269
2270    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2271    let (_worktree, _) = project
2272        .update(cx, |project, cx| {
2273            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2274        })
2275        .await
2276        .unwrap();
2277
2278    let (worktree_2, _) = project
2279        .update(cx, |project, cx| {
2280            project.find_or_create_worktree(path!("/code/project2"), true, cx)
2281        })
2282        .await
2283        .unwrap();
2284    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
2285
2286    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
2287
2288    let (worktree_2, _) = project
2289        .update(cx, |project, cx| {
2290            project.find_or_create_worktree(path!("/code/project2"), true, cx)
2291        })
2292        .await
2293        .unwrap();
2294
2295    cx.run_until_parked();
2296    worktree_2.update(cx, |worktree, _cx| {
2297        assert!(worktree.is_visible());
2298        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
2299        assert_eq!(entries.len(), 2);
2300        assert_eq!(entries[1].path.as_unix_str(), "README.md")
2301    })
2302}
2303
2304#[gpui::test]
2305async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2306    let fs = FakeFs::new(server_cx.executor());
2307    fs.insert_tree(
2308        path!("/code"),
2309        json!({
2310            "project1": {
2311                ".git": {},
2312                "README.md": "# project 1",
2313                "src": {
2314                    "lib.rs": "fn one() -> usize { 1 }"
2315                }
2316            },
2317        }),
2318    )
2319    .await;
2320
2321    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2322    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
2323    cx.executor().run_until_parked();
2324
2325    let buffer = buffer.await.unwrap();
2326
2327    cx.update(|cx| {
2328        assert_eq!(
2329            buffer.read(cx).text(),
2330            initial_server_settings_content()
2331                .to_string()
2332                .replace("\r\n", "\n")
2333        )
2334    })
2335}
2336
2337#[gpui::test(iterations = 20)]
2338async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2339    let fs = FakeFs::new(server_cx.executor());
2340    fs.insert_tree(
2341        path!("/code"),
2342        json!({
2343            "project1": {
2344                ".git": {},
2345                "README.md": "# project 1",
2346                "src": {
2347                    "lib.rs": "fn one() -> usize { 1 }"
2348                }
2349            },
2350        }),
2351    )
2352    .await;
2353
2354    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2355
2356    let (worktree, _) = project
2357        .update(cx, |project, cx| {
2358            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2359        })
2360        .await
2361        .unwrap();
2362
2363    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2364    let buffer = project
2365        .update(cx, |project, cx| {
2366            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2367        })
2368        .await
2369        .unwrap();
2370
2371    buffer.update(cx, |buffer, cx| {
2372        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
2373        let ix = buffer.text().find('1').unwrap();
2374        buffer.edit([(ix..ix + 1, "100")], None, cx);
2375    });
2376
2377    let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
2378    client
2379        .update(cx, |client, cx| client.simulate_disconnect(cx))
2380        .detach();
2381
2382    project
2383        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2384        .await
2385        .unwrap();
2386
2387    assert_eq!(
2388        fs.load(path!("/code/project1/src/lib.rs").as_ref())
2389            .await
2390            .unwrap(),
2391        "fn one() -> usize { 100 }"
2392    );
2393}
2394
2395#[gpui::test]
2396async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2397    let fs = FakeFs::new(server_cx.executor());
2398    fs.insert_tree(
2399        "/code",
2400        json!({
2401            "project1": {
2402                ".git": {},
2403                "README.md": "# project 1",
2404            },
2405        }),
2406    )
2407    .await;
2408
2409    let (project, _) = init_test(&fs, cx, server_cx).await;
2410
2411    let (worktree, _) = project
2412        .update(cx, |project, cx| {
2413            project.find_or_create_worktree("/code/project1", true, cx)
2414        })
2415        .await
2416        .unwrap();
2417
2418    cx.run_until_parked();
2419
2420    fs.rename(
2421        &PathBuf::from("/code/project1"),
2422        &PathBuf::from("/code/project2"),
2423        Default::default(),
2424    )
2425    .await
2426    .unwrap();
2427
2428    cx.run_until_parked();
2429    worktree.update(cx, |worktree, _| {
2430        assert_eq!(worktree.root_name(), "project2")
2431    })
2432}
2433
2434#[gpui::test]
2435async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2436    let fs = FakeFs::new(server_cx.executor());
2437    fs.insert_tree(
2438        "/code",
2439        json!({
2440            "project1": {
2441                ".git": {},
2442                "README.md": "# project 1",
2443            },
2444        }),
2445    )
2446    .await;
2447
2448    let (project, _) = init_test(&fs, cx, server_cx).await;
2449    let (worktree, _) = project
2450        .update(cx, |project, cx| {
2451            project.find_or_create_worktree("/code/project1", true, cx)
2452        })
2453        .await
2454        .unwrap();
2455
2456    cx.run_until_parked();
2457
2458    let entry = project
2459        .update(cx, |project, cx| {
2460            let worktree = worktree.read(cx);
2461            let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
2462            project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
2463        })
2464        .await
2465        .unwrap()
2466        .into_included()
2467        .unwrap();
2468
2469    cx.run_until_parked();
2470
2471    worktree.update(cx, |worktree, _| {
2472        assert_eq!(
2473            worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
2474            entry.id
2475        )
2476    });
2477}
2478
2479#[gpui::test]
2480async fn test_copy_file_into_remote_project(
2481    cx: &mut TestAppContext,
2482    server_cx: &mut TestAppContext,
2483) {
2484    let remote_fs = FakeFs::new(server_cx.executor());
2485    remote_fs
2486        .insert_tree(
2487            path!("/code"),
2488            json!({
2489                "project1": {
2490                    ".git": {},
2491                    "README.md": "# project 1",
2492                    "src": {
2493                        "main.rs": ""
2494                    }
2495                },
2496            }),
2497        )
2498        .await;
2499
2500    let (project, _) = init_test(&remote_fs, cx, server_cx).await;
2501    let (worktree, _) = project
2502        .update(cx, |project, cx| {
2503            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2504        })
2505        .await
2506        .unwrap();
2507
2508    cx.run_until_parked();
2509
2510    let local_fs = project
2511        .read_with(cx, |project, _| project.fs().clone())
2512        .as_fake();
2513    local_fs
2514        .insert_tree(
2515            path!("/local-code"),
2516            json!({
2517                "dir1": {
2518                    "file1": "file 1 content",
2519                    "dir2": {
2520                        "file2": "file 2 content",
2521                        "dir3": {
2522                            "file3": ""
2523                        },
2524                        "dir4": {}
2525                    },
2526                    "dir5": {}
2527                },
2528                "file4": "file 4 content"
2529            }),
2530        )
2531        .await;
2532
2533    worktree
2534        .update(cx, |worktree, cx| {
2535            worktree.copy_external_entries(
2536                rel_path("src").into(),
2537                vec![
2538                    Path::new(path!("/local-code/dir1/file1")).into(),
2539                    Path::new(path!("/local-code/dir1/dir2")).into(),
2540                ],
2541                local_fs.clone(),
2542                cx,
2543            )
2544        })
2545        .await
2546        .unwrap();
2547
2548    assert_eq!(
2549        remote_fs.paths(true),
2550        vec![
2551            PathBuf::from(path!("/")),
2552            PathBuf::from(path!("/code")),
2553            PathBuf::from(path!("/code/project1")),
2554            PathBuf::from(path!("/code/project1/.git")),
2555            PathBuf::from(path!("/code/project1/README.md")),
2556            PathBuf::from(path!("/code/project1/src")),
2557            PathBuf::from(path!("/code/project1/src/dir2")),
2558            PathBuf::from(path!("/code/project1/src/file1")),
2559            PathBuf::from(path!("/code/project1/src/main.rs")),
2560            PathBuf::from(path!("/code/project1/src/dir2/dir3")),
2561            PathBuf::from(path!("/code/project1/src/dir2/dir4")),
2562            PathBuf::from(path!("/code/project1/src/dir2/file2")),
2563            PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
2564        ]
2565    );
2566    assert_eq!(
2567        remote_fs
2568            .load(path!("/code/project1/src/file1").as_ref())
2569            .await
2570            .unwrap(),
2571        "file 1 content"
2572    );
2573    assert_eq!(
2574        remote_fs
2575            .load(path!("/code/project1/src/dir2/file2").as_ref())
2576            .await
2577            .unwrap(),
2578        "file 2 content"
2579    );
2580    assert_eq!(
2581        remote_fs
2582            .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
2583            .await
2584            .unwrap(),
2585        ""
2586    );
2587}
2588
2589#[gpui::test]
2590async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2591    let fs = FakeFs::new(server_cx.executor());
2592    fs.insert_tree(
2593        "/code",
2594        json!({
2595            "main_repo": {
2596                ".git": {},
2597                "file.txt": "content",
2598            },
2599            "no_git": {
2600                "file.txt": "content",
2601            },
2602        }),
2603    )
2604    .await;
2605
2606    // Create a linked worktree that points back to main_repo's .git.
2607    fs.add_linked_worktree_for_repo(
2608        Path::new("/code/main_repo/.git"),
2609        false,
2610        GitWorktree {
2611            path: PathBuf::from("/code/linked_worktree"),
2612            ref_name: Some("refs/heads/feature-branch".into()),
2613            sha: "abc123".into(),
2614            is_main: false,
2615            is_bare: false,
2616        },
2617    )
2618    .await;
2619
2620    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2621
2622    // Main repo: root_repo_common_dir should be the .git directory itself.
2623    let (worktree_main, _) = project
2624        .update(cx, |project, cx| {
2625            project.find_or_create_worktree("/code/main_repo", true, cx)
2626        })
2627        .await
2628        .unwrap();
2629    cx.executor().run_until_parked();
2630
2631    let (common_dir, is_linked_worktree) = worktree_main.read_with(cx, |worktree, _| {
2632        (
2633            worktree.snapshot().root_repo_common_dir().cloned(),
2634            worktree.snapshot().root_repo_is_linked_worktree(),
2635        )
2636    });
2637    assert_eq!(
2638        common_dir.as_deref(),
2639        Some(Path::new("/code/main_repo/.git")),
2640    );
2641    assert!(!is_linked_worktree);
2642
2643    // Linked worktree: root_repo_common_dir should point to the main repo's .git.
2644    let (worktree_linked, _) = project
2645        .update(cx, |project, cx| {
2646            project.find_or_create_worktree("/code/linked_worktree", true, cx)
2647        })
2648        .await
2649        .unwrap();
2650    cx.executor().run_until_parked();
2651
2652    let (common_dir, is_linked_worktree) = worktree_linked.read_with(cx, |worktree, _| {
2653        (
2654            worktree.snapshot().root_repo_common_dir().cloned(),
2655            worktree.snapshot().root_repo_is_linked_worktree(),
2656        )
2657    });
2658    assert_eq!(
2659        common_dir.as_deref(),
2660        Some(Path::new("/code/main_repo/.git")),
2661    );
2662    assert!(is_linked_worktree);
2663
2664    let main_worktree_paths = project.read_with(cx, |project, cx| {
2665        project
2666            .worktree_paths(cx)
2667            .main_worktree_path_list()
2668            .ordered_paths()
2669            .map(|path| path.to_path_buf())
2670            .collect::<Vec<_>>()
2671    });
2672    assert_eq!(
2673        main_worktree_paths,
2674        vec![
2675            PathBuf::from("/code/main_repo"),
2676            PathBuf::from("/code/main_repo"),
2677        ]
2678    );
2679
2680    // No git repo: root_repo_common_dir should be None.
2681    let (worktree_no_git, _) = project
2682        .update(cx, |project, cx| {
2683            project.find_or_create_worktree("/code/no_git", true, cx)
2684        })
2685        .await
2686        .unwrap();
2687    cx.executor().run_until_parked();
2688
2689    let (common_dir, is_linked_worktree) = worktree_no_git.read_with(cx, |worktree, _| {
2690        (
2691            worktree.snapshot().root_repo_common_dir().cloned(),
2692            worktree.snapshot().root_repo_is_linked_worktree(),
2693        )
2694    });
2695    assert_eq!(common_dir, None);
2696    assert!(!is_linked_worktree);
2697}
2698
2699#[gpui::test]
2700async fn test_remote_search_commits_streams_proto_chunks(
2701    cx: &mut TestAppContext,
2702    server_cx: &mut TestAppContext,
2703) {
2704    const COMMIT_COUNT: usize = 900;
2705    const RESPONSE_MAX_SIZE: usize = 100;
2706
2707    let fs = FakeFs::new(server_cx.executor());
2708    fs.insert_tree(
2709        path!("/code"),
2710        json!({
2711            "project1": {
2712                ".git": {},
2713                "file.txt": "content",
2714            },
2715        }),
2716    )
2717    .await;
2718
2719    let commit_data = (0..COMMIT_COUNT)
2720        .map(|index| {
2721            let sha = Oid::from_str(&format!("{:040x}", index + 1)).unwrap();
2722            (
2723                CommitData {
2724                    sha,
2725                    parents: Default::default(),
2726                    author_name: SharedString::from("Author"),
2727                    author_email: SharedString::from("author@example.com"),
2728                    commit_timestamp: index as i64,
2729                    subject: SharedString::from(format!("Subject {index}")),
2730                    message: SharedString::from(format!("needle commit {index}")),
2731                },
2732                false,
2733            )
2734        })
2735        .collect::<Vec<_>>();
2736    let expected_shas = commit_data
2737        .iter()
2738        .map(|(commit_data, _)| commit_data.sha.to_string())
2739        .collect::<HashSet<_>>();
2740    fs.set_commit_data(Path::new(path!("/code/project1/.git")), commit_data);
2741
2742    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2743    project
2744        .update(cx, |project, cx| {
2745            project.find_or_create_worktree(path!("/code/project1"), true, cx)
2746        })
2747        .await
2748        .expect("should open remote worktree");
2749    server_cx.run_until_parked();
2750    cx.run_until_parked();
2751    project
2752        .update(cx, |project, cx| project.git_scans_complete(cx))
2753        .await;
2754
2755    let (remote_client, repository_id) = project.read_with(cx, |project, cx| {
2756        let repository = project
2757            .active_repository(cx)
2758            .expect("remote project should have an active repository");
2759        let repository_id = repository.read(cx).snapshot().id;
2760        let remote_client = project
2761            .remote_client()
2762            .expect("project should have a remote client");
2763        (remote_client, repository_id)
2764    });
2765    let proto_client = remote_client.read_with(cx, |remote_client, _| remote_client.proto_client());
2766    let mut stream = proto_client
2767        .request_stream(proto::SearchCommits {
2768            project_id: proto::REMOTE_SERVER_PROJECT_ID,
2769            repository_id: repository_id.to_proto(),
2770            log_source: Some(proto::GitLogSource {
2771                source: Some(proto::git_log_source::Source::All(
2772                    proto::GitLogSourceAll {},
2773                )),
2774            }),
2775            query: "needle".to_string(),
2776            case_sensitive: true,
2777        })
2778        .await
2779        .expect("search commits stream should start");
2780
2781    let mut chunks = Vec::new();
2782    while let Some(response) = futures::StreamExt::next(&mut stream).await {
2783        chunks.push(response.expect("search commits chunk should succeed").shas);
2784    }
2785
2786    assert!(
2787        chunks.len() > 1,
2788        "expected search results to stream in multiple chunks"
2789    );
2790    for chunk in chunks.iter().take(chunks.len() - 1) {
2791        assert!(
2792            chunk.len() <= RESPONSE_MAX_SIZE,
2793            "non-final chunks should meet the target byte size"
2794        );
2795    }
2796
2797    let actual_shas = chunks.into_iter().flatten().collect::<HashSet<_>>();
2798    assert_eq!(actual_shas, expected_shas);
2799}
2800
2801#[gpui::test]
2802async fn test_remote_archive_git_operations_are_supported(
2803    cx: &mut TestAppContext,
2804    server_cx: &mut TestAppContext,
2805) {
2806    let fs = FakeFs::new(server_cx.executor());
2807    fs.insert_tree(
2808        "/project",
2809        json!({
2810            ".git": {},
2811            "file.txt": "content",
2812        }),
2813    )
2814    .await;
2815    fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2816    fs.set_head_for_repo(
2817        Path::new("/project/.git"),
2818        &[("file.txt", "content".into())],
2819        "head-sha",
2820    );
2821
2822    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2823    project
2824        .update(cx, |project, cx| {
2825            project.find_or_create_worktree(Path::new("/project"), true, cx)
2826        })
2827        .await
2828        .expect("should open remote worktree");
2829    cx.run_until_parked();
2830
2831    let repository = project.read_with(cx, |project, cx| {
2832        project
2833            .active_repository(cx)
2834            .expect("remote project should have an active repository")
2835    });
2836
2837    let head_sha = cx
2838        .update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
2839        .await
2840        .expect("head_sha request should complete")
2841        .expect("head_sha should succeed")
2842        .expect("HEAD should exist");
2843
2844    cx.run_until_parked();
2845
2846    cx.update(|cx| {
2847        repository.update(cx, |repository, _| {
2848            repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
2849        })
2850    })
2851    .await
2852    .expect("update_ref request should complete")
2853    .expect("update_ref should succeed for remote repository");
2854
2855    cx.run_until_parked();
2856
2857    cx.update(|cx| {
2858        repository.update(cx, |repository, _| {
2859            repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
2860        })
2861    })
2862    .await
2863    .expect("delete_ref request should complete")
2864    .expect("delete_ref should succeed for remote repository");
2865
2866    cx.run_until_parked();
2867
2868    cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
2869        .await
2870        .expect("repair_worktrees request should complete")
2871        .expect("repair_worktrees should succeed for remote repository");
2872
2873    cx.run_until_parked();
2874
2875    let (staged_commit_sha, unstaged_commit_sha) = cx
2876        .update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
2877        .await
2878        .expect("create_archive_checkpoint request should complete")
2879        .expect("create_archive_checkpoint should succeed for remote repository");
2880
2881    cx.run_until_parked();
2882
2883    cx.update(|cx| {
2884        repository.update(cx, |repository, _| {
2885            repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
2886        })
2887    })
2888    .await
2889    .expect("restore_archive_checkpoint request should complete")
2890    .expect("restore_archive_checkpoint should succeed for remote repository");
2891}
2892
2893#[gpui::test]
2894async fn test_add_path_to_gitignore_in_remote_repository(
2895    cx: &mut TestAppContext,
2896    server_cx: &mut TestAppContext,
2897) {
2898    let fs = FakeFs::new(server_cx.executor());
2899    fs.insert_tree(
2900        "/project",
2901        json!({
2902            ".git": {},
2903            ".gitignore": "existing\n",
2904            "logs": {
2905                "app.log": ""
2906            },
2907            "tmp.txt": "",
2908        }),
2909    )
2910    .await;
2911    fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2912    fs.set_head_for_repo(
2913        Path::new("/project/.git"),
2914        &[(".gitignore", "existing\n".into())],
2915        "head-sha",
2916    );
2917
2918    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2919    project
2920        .update(cx, |project, cx| {
2921            project.find_or_create_worktree(Path::new("/project"), true, cx)
2922        })
2923        .await
2924        .expect("should open remote worktree");
2925    cx.run_until_parked();
2926
2927    let repository = project.read_with(cx, |project, cx| {
2928        project
2929            .active_repository(cx)
2930            .expect("remote project should have an active repository")
2931    });
2932
2933    for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
2934        let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
2935        cx.update(|cx| {
2936            repository.update(cx, |repository, _| {
2937                repository.add_path_to_gitignore(&repo_path, is_dir)
2938            })
2939        })
2940        .await
2941        .expect("add to .gitignore request should complete")
2942        .expect("add to .gitignore should succeed for remote repository");
2943    }
2944
2945    server_cx.run_until_parked();
2946    cx.run_until_parked();
2947
2948    assert_eq!(
2949        fs.load(Path::new("/project/.gitignore"))
2950            .await
2951            .expect(".gitignore should be readable"),
2952        "existing\ntmp.txt\nlogs/\n"
2953    );
2954}
2955
2956#[gpui::test]
2957async fn test_add_path_to_git_info_exclude_in_remote_repository(
2958    cx: &mut TestAppContext,
2959    server_cx: &mut TestAppContext,
2960) {
2961    let fs = FakeFs::new(server_cx.executor());
2962    fs.insert_tree(
2963        "/project",
2964        json!({
2965            ".git": {},
2966            "logs": {
2967                "app.log": ""
2968            },
2969            "tmp.txt": "",
2970        }),
2971    )
2972    .await;
2973    fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2974    fs.set_head_for_repo(Path::new("/project/.git"), &[], "head-sha");
2975
2976    let (project, _headless) = init_test(&fs, cx, server_cx).await;
2977    project
2978        .update(cx, |project, cx| {
2979            project.find_or_create_worktree(Path::new("/project"), true, cx)
2980        })
2981        .await
2982        .expect("should open remote worktree");
2983    cx.run_until_parked();
2984
2985    let repository = project.read_with(cx, |project, cx| {
2986        project
2987            .active_repository(cx)
2988            .expect("remote project should have an active repository")
2989    });
2990
2991    for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
2992        let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
2993        cx.update(|cx| {
2994            repository.update(cx, |repository, _| {
2995                repository.add_path_to_git_info_exclude(&repo_path, is_dir)
2996            })
2997        })
2998        .await
2999        .expect("add to info/exclude request should complete")
3000        .expect("add to info/exclude should succeed for remote repository");
3001    }
3002
3003    server_cx.run_until_parked();
3004    cx.run_until_parked();
3005
3006    assert_eq!(
3007        fs.load(Path::new("/project/.git/info/exclude"))
3008            .await
3009            .expect("info/exclude should be readable"),
3010        "tmp.txt\nlogs/\n"
3011    );
3012}
3013
3014#[gpui::test]
3015async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3016    let text_2 = "
3017        fn one() -> usize {
3018            1
3019        }
3020    "
3021    .unindent();
3022    let text_1 = "
3023        fn one() -> usize {
3024            0
3025        }
3026    "
3027    .unindent();
3028
3029    let fs = FakeFs::new(server_cx.executor());
3030    fs.insert_tree(
3031        "/code",
3032        json!({
3033            "project1": {
3034                ".git": {},
3035                "src": {
3036                    "lib.rs": text_2
3037                },
3038                "README.md": "# project 1",
3039            },
3040        }),
3041    )
3042    .await;
3043    fs.set_index_for_repo(
3044        Path::new("/code/project1/.git"),
3045        &[("src/lib.rs", text_1.clone())],
3046    );
3047    fs.set_head_for_repo(
3048        Path::new("/code/project1/.git"),
3049        &[("src/lib.rs", text_1.clone())],
3050        "deadbeef",
3051    );
3052
3053    let (project, _headless) = init_test(&fs, cx, server_cx).await;
3054    let (worktree, _) = project
3055        .update(cx, |project, cx| {
3056            project.find_or_create_worktree("/code/project1", true, cx)
3057        })
3058        .await
3059        .unwrap();
3060    let worktree_id = cx.update(|cx| worktree.read(cx).id());
3061    cx.executor().run_until_parked();
3062
3063    let buffer = project
3064        .update(cx, |project, cx| {
3065            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
3066        })
3067        .await
3068        .unwrap();
3069    let diff = project
3070        .update(cx, |project, cx| {
3071            project.open_uncommitted_diff(buffer.clone(), cx)
3072        })
3073        .await
3074        .unwrap();
3075
3076    diff.read_with(cx, |diff, cx| {
3077        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3078        assert_eq!(
3079            diff.secondary_diff()
3080                .unwrap()
3081                .read(cx)
3082                .base_text_string(cx)
3083                .unwrap(),
3084            text_1
3085        );
3086    });
3087
3088    // stage the current buffer's contents
3089    fs.set_index_for_repo(
3090        Path::new("/code/project1/.git"),
3091        &[("src/lib.rs", text_2.clone())],
3092    );
3093
3094    cx.executor().run_until_parked();
3095    diff.read_with(cx, |diff, cx| {
3096        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3097        assert_eq!(
3098            diff.secondary_diff()
3099                .unwrap()
3100                .read(cx)
3101                .base_text_string(cx)
3102                .unwrap(),
3103            text_2
3104        );
3105    });
3106
3107    // commit the current buffer's contents
3108    fs.set_head_for_repo(
3109        Path::new("/code/project1/.git"),
3110        &[("src/lib.rs", text_2.clone())],
3111        "deadbeef",
3112    );
3113
3114    cx.executor().run_until_parked();
3115    diff.read_with(cx, |diff, cx| {
3116        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
3117        assert_eq!(
3118            diff.secondary_diff()
3119                .unwrap()
3120                .read(cx)
3121                .base_text_string(cx)
3122                .unwrap(),
3123            text_2
3124        );
3125    });
3126}
3127
3128#[gpui::test]
3129async fn test_remote_git_diffs_when_recv_update_repository_delay(
3130    cx: &mut TestAppContext,
3131    server_cx: &mut TestAppContext,
3132) {
3133    cx.update(|cx| {
3134        let settings_store = SettingsStore::test(cx);
3135        cx.set_global(settings_store);
3136        theme_settings::init(theme::LoadThemes::JustBase, cx);
3137        release_channel::init(semver::Version::new(0, 0, 0), cx);
3138        editor::init(cx);
3139    });
3140
3141    use editor::Editor;
3142    use gpui::VisualContext;
3143    let text_2 = "
3144        fn one() -> usize {
3145            1
3146        }
3147    "
3148    .unindent();
3149    let text_1 = "
3150        fn one() -> usize {
3151            0
3152        }
3153    "
3154    .unindent();
3155
3156    let fs = FakeFs::new(server_cx.executor());
3157    fs.insert_tree(
3158        path!("/code"),
3159        json!({
3160            "project1": {
3161                "src": {
3162                    "lib.rs": text_2
3163                },
3164                "README.md": "# project 1",
3165            },
3166        }),
3167    )
3168    .await;
3169
3170    let (project, _headless) = init_test(&fs, cx, server_cx).await;
3171    let (worktree, _) = project
3172        .update(cx, |project, cx| {
3173            project.find_or_create_worktree(path!("/code/project1"), true, cx)
3174        })
3175        .await
3176        .unwrap();
3177    let worktree_id = cx.update(|cx| worktree.read(cx).id());
3178    let buffer = project
3179        .update(cx, |project, cx| {
3180            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
3181        })
3182        .await
3183        .unwrap();
3184    let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
3185
3186    let cx = cx.add_empty_window();
3187    let editor = cx.new_window_entity(|window, cx| {
3188        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
3189    });
3190
3191    // Remote server will send proto::UpdateRepository after the instance of Editor create.
3192    fs.insert_tree(
3193        path!("/code"),
3194        json!({
3195            "project1": {
3196                ".git": {},
3197            },
3198        }),
3199    )
3200    .await;
3201
3202    fs.set_index_for_repo(
3203        Path::new(path!("/code/project1/.git")),
3204        &[("src/lib.rs", text_1.clone())],
3205    );
3206    fs.set_head_for_repo(
3207        Path::new(path!("/code/project1/.git")),
3208        &[("src/lib.rs", text_1.clone())],
3209        "sha",
3210    );
3211
3212    cx.executor().run_until_parked();
3213    let diff = editor
3214        .read_with(cx, |editor, cx| {
3215            editor
3216                .buffer()
3217                .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
3218        })
3219        .unwrap();
3220
3221    diff.read_with(cx, |diff, cx| {
3222        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3223        assert_eq!(
3224            diff.secondary_diff()
3225                .unwrap()
3226                .read(cx)
3227                .base_text_string(cx)
3228                .unwrap(),
3229            text_1
3230        );
3231    });
3232
3233    // stage the current buffer's contents
3234    fs.set_index_for_repo(
3235        Path::new(path!("/code/project1/.git")),
3236        &[("src/lib.rs", text_2.clone())],
3237    );
3238
3239    cx.executor().run_until_parked();
3240    diff.read_with(cx, |diff, cx| {
3241        assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3242        assert_eq!(
3243            diff.secondary_diff()
3244                .unwrap()
3245                .read(cx)
3246                .base_text_string(cx)
3247                .unwrap(),
3248            text_2
3249        );
3250    });
3251
3252    // commit the current buffer's contents
3253    fs.set_head_for_repo(
3254        Path::new(path!("/code/project1/.git")),
3255        &[("src/lib.rs", text_2.clone())],
3256        "sha",
3257    );
3258
3259    cx.executor().run_until_parked();
3260    diff.read_with(cx, |diff, cx| {
3261        assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
3262        assert_eq!(
3263            diff.secondary_diff()
3264                .unwrap()
3265                .read(cx)
3266                .base_text_string(cx)
3267                .unwrap(),
3268            text_2
3269        );
3270    });
3271}
3272
3273#[gpui::test]
3274async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3275    let fs = FakeFs::new(server_cx.executor());
3276    fs.insert_tree(
3277        path!("/code"),
3278        json!({
3279            "project1": {
3280                ".git": {},
3281                "README.md": "# project 1",
3282            },
3283        }),
3284    )
3285    .await;
3286
3287    let (project, headless_project) = init_test(&fs, cx, server_cx).await;
3288    let branches = ["main", "dev", "feature-1"];
3289    let branches_set = branches
3290        .iter()
3291        .map(ToString::to_string)
3292        .collect::<HashSet<_>>();
3293    fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
3294
3295    let (_worktree, _) = project
3296        .update(cx, |project, cx| {
3297            project.find_or_create_worktree(path!("/code/project1"), true, cx)
3298        })
3299        .await
3300        .unwrap();
3301    // Give the worktree a bit of time to index the file system
3302    cx.run_until_parked();
3303
3304    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
3305
3306    let remote_branches = repository
3307        .update(cx, |repository, _| repository.branches())
3308        .await
3309        .unwrap()
3310        .unwrap()
3311        .branches;
3312
3313    let new_branch = branches[2];
3314
3315    let remote_branches = remote_branches
3316        .into_iter()
3317        .map(|branch| branch.name().to_string())
3318        .collect::<HashSet<_>>();
3319
3320    assert_eq!(&remote_branches, &branches_set);
3321
3322    cx.update(|cx| {
3323        repository.update(cx, |repository, _cx| {
3324            repository.change_branch(new_branch.to_string())
3325        })
3326    })
3327    .await
3328    .unwrap()
3329    .unwrap();
3330
3331    cx.run_until_parked();
3332
3333    let server_branch = server_cx.update(|cx| {
3334        headless_project.update(cx, |headless_project, cx| {
3335            headless_project.git_store.update(cx, |git_store, cx| {
3336                git_store
3337                    .repositories()
3338                    .values()
3339                    .next()
3340                    .unwrap()
3341                    .read(cx)
3342                    .branch
3343                    .as_ref()
3344                    .unwrap()
3345                    .clone()
3346            })
3347        })
3348    });
3349
3350    assert_eq!(server_branch.name(), branches[2]);
3351
3352    // Also try creating a new branch
3353    cx.update(|cx| {
3354        repository.update(cx, |repo, _cx| {
3355            repo.create_branch("totally-new-branch".to_string(), None)
3356        })
3357    })
3358    .await
3359    .unwrap()
3360    .unwrap();
3361
3362    cx.update(|cx| {
3363        repository.update(cx, |repo, _cx| {
3364            repo.change_branch("totally-new-branch".to_string())
3365        })
3366    })
3367    .await
3368    .unwrap()
3369    .unwrap();
3370
3371    cx.run_until_parked();
3372
3373    let server_branch = server_cx.update(|cx| {
3374        headless_project.update(cx, |headless_project, cx| {
3375            headless_project.git_store.update(cx, |git_store, cx| {
3376                git_store
3377                    .repositories()
3378                    .values()
3379                    .next()
3380                    .unwrap()
3381                    .read(cx)
3382                    .branch
3383                    .as_ref()
3384                    .unwrap()
3385                    .clone()
3386            })
3387        })
3388    });
3389
3390    assert_eq!(server_branch.name(), "totally-new-branch");
3391
3392    let default_branch = cx
3393        .update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(false)))
3394        .await
3395        .unwrap()
3396        .unwrap();
3397    assert_eq!(default_branch.as_deref(), Some("main"));
3398
3399    let default_branch_with_remote = cx
3400        .update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(true)))
3401        .await
3402        .unwrap()
3403        .unwrap();
3404    assert_eq!(default_branch_with_remote.as_deref(), Some("origin/main"));
3405}
3406
3407#[gpui::test]
3408async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3409    let fs = FakeFs::new(server_cx.executor());
3410    fs.insert_tree(
3411        path!("/code"),
3412        json!({
3413            "project1": {
3414                ".git": {},
3415                "file.txt": "original content",
3416            },
3417        }),
3418    )
3419    .await;
3420
3421    let (project, _headless) = init_test(&fs, cx, server_cx).await;
3422
3423    let (_worktree, _) = project
3424        .update(cx, |project, cx| {
3425            project.find_or_create_worktree(path!("/code/project1"), true, cx)
3426        })
3427        .await
3428        .unwrap();
3429    cx.run_until_parked();
3430
3431    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
3432
3433    // 1. Create a checkpoint of the original state
3434    let checkpoint_1 = repository
3435        .update(cx, |repository, _| repository.checkpoint())
3436        .await
3437        .unwrap()
3438        .unwrap();
3439
3440    // 2. Modify a file on the server-side fs
3441    fs.write(
3442        Path::new(path!("/code/project1/file.txt")),
3443        b"modified content",
3444    )
3445    .await
3446    .unwrap();
3447
3448    // 3. Create a second checkpoint with the modified state
3449    let checkpoint_2 = repository
3450        .update(cx, |repository, _| repository.checkpoint())
3451        .await
3452        .unwrap()
3453        .unwrap();
3454
3455    // 4. compare_checkpoints: same checkpoint with itself => equal
3456    let equal = repository
3457        .update(cx, |repository, _| {
3458            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
3459        })
3460        .await
3461        .unwrap()
3462        .unwrap();
3463    assert!(equal, "a checkpoint compared with itself should be equal");
3464
3465    // 5. compare_checkpoints: different states => not equal
3466    let equal = repository
3467        .update(cx, |repository, _| {
3468            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
3469        })
3470        .await
3471        .unwrap()
3472        .unwrap();
3473    assert!(
3474        !equal,
3475        "checkpoints of different states should not be equal"
3476    );
3477
3478    // 6. diff_checkpoints: same checkpoint => empty diff
3479    let diff = repository
3480        .update(cx, |repository, _| {
3481            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
3482        })
3483        .await
3484        .unwrap()
3485        .unwrap();
3486    assert!(
3487        diff.is_empty(),
3488        "diff of identical checkpoints should be empty"
3489    );
3490
3491    // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
3492    let diff = repository
3493        .update(cx, |repository, _| {
3494            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
3495        })
3496        .await
3497        .unwrap()
3498        .unwrap();
3499    assert!(
3500        !diff.is_empty(),
3501        "diff of different checkpoints should be non-empty"
3502    );
3503    assert!(
3504        diff.contains("file.txt"),
3505        "diff should mention the changed file"
3506    );
3507    assert!(
3508        diff.contains("original content"),
3509        "diff should contain removed content"
3510    );
3511    assert!(
3512        diff.contains("modified content"),
3513        "diff should contain added content"
3514    );
3515
3516    // 8. restore_checkpoint: restore to original state
3517    repository
3518        .update(cx, |repository, _| {
3519            repository.restore_checkpoint(checkpoint_1.clone())
3520        })
3521        .await
3522        .unwrap()
3523        .unwrap();
3524    cx.run_until_parked();
3525
3526    // 9. Create a checkpoint after restore
3527    let checkpoint_3 = repository
3528        .update(cx, |repository, _| repository.checkpoint())
3529        .await
3530        .unwrap()
3531        .unwrap();
3532
3533    // 10. compare_checkpoints: restored state matches original
3534    let equal = repository
3535        .update(cx, |repository, _| {
3536            repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
3537        })
3538        .await
3539        .unwrap()
3540        .unwrap();
3541    assert!(equal, "restored state should match original checkpoint");
3542
3543    // 11. diff_checkpoints: restored state vs original => empty diff
3544    let diff = repository
3545        .update(cx, |repository, _| {
3546            repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
3547        })
3548        .await
3549        .unwrap()
3550        .unwrap();
3551    assert!(diff.is_empty(), "diff after restore should be empty");
3552}
3553
3554#[gpui::test]
3555async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3556    let fs = FakeFs::new(server_cx.executor());
3557    fs.insert_tree(
3558        path!("/project"),
3559        json!({
3560            "a.txt": "A",
3561            "b.txt": "B",
3562        }),
3563    )
3564    .await;
3565
3566    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3567    project
3568        .update(cx, |project, cx| {
3569            project.find_or_create_worktree(path!("/project"), true, cx)
3570        })
3571        .await
3572        .unwrap();
3573
3574    let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
3575
3576    let input = ReadFileToolInput {
3577        path: "project/b.txt".into(),
3578        start_line: None,
3579        end_line: None,
3580    };
3581    let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
3582    let (event_stream, _) = ToolCallEventStream::test();
3583
3584    let exists_result = cx.update(|cx| {
3585        read_tool
3586            .clone()
3587            .run(ToolInput::resolved(input), event_stream.clone(), cx)
3588    });
3589    let output = exists_result.await.unwrap();
3590    assert_eq!(
3591        output,
3592        LanguageModelToolResultContent::Text("     1\tB".into())
3593    );
3594
3595    let input = ReadFileToolInput {
3596        path: "project/c.txt".into(),
3597        start_line: None,
3598        end_line: None,
3599    };
3600    let does_not_exist_result =
3601        cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
3602    does_not_exist_result.await.unwrap_err();
3603}
3604
3605#[gpui::test]
3606async fn test_adding_remote_skill(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3607    use acp_thread::AgentConnection as _;
3608
3609    let fs = FakeFs::new(server_cx.executor());
3610    fs.insert_tree(
3611        path!("/project"),
3612        json!({
3613            ".agents": {
3614                "skills": {
3615                    "test-skill": {
3616                        "SKILL.md": "---\nname: test-skill\ndescription: test description\n---\ntest body"
3617                    }
3618                }
3619            }
3620        }),
3621    )
3622    .await;
3623
3624    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3625    cx.update(|cx| {
3626        LanguageModelRegistry::test(cx);
3627    });
3628    let (_worktree, _rel_path) = project
3629        .update(cx, |project, cx| {
3630            project.find_or_create_worktree(path!("/project"), true, cx)
3631        })
3632        .await
3633        .unwrap();
3634    cx.run_until_parked();
3635    let thread_store = cx.new(|cx| ThreadStore::new(cx));
3636    let agent = cx.update(|cx| NativeAgent::new(thread_store, Templates::new(), fs.clone(), cx));
3637    let connection = Rc::new(NativeAgentConnection(agent.clone()));
3638    let _acp_thread = cx
3639        .update(|cx| {
3640            connection.clone().new_session(
3641                project.clone(),
3642                PathList::new(&[Path::new("/project")]),
3643                cx,
3644            )
3645        })
3646        .await
3647        .unwrap();
3648    cx.run_until_parked();
3649
3650    let skill_tool = Arc::new(SkillTool::with_body_resolver(
3651        skills_resolver_for_project(agent.downgrade(), project.entity_id()),
3652        skill_body_resolver_for_project(project.clone(), fs.clone()),
3653    ));
3654    let (event_stream, mut event_stream_rx) = ToolCallEventStream::test();
3655
3656    let input = SkillToolInput {
3657        name: "test-skill".into(),
3658    };
3659    let task = cx.update(|cx| {
3660        skill_tool
3661            .clone()
3662            .run(ToolInput::resolved(input), event_stream.clone(), cx)
3663    });
3664
3665    // The project-local skill is not a built-in, so the tool requests
3666    // authorization. Approve it so the tool can proceed.
3667    let authorization = event_stream_rx.expect_authorization().await;
3668    authorization
3669        .response
3670        .send(acp_thread::SelectedPermissionOutcome::new(
3671            agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
3672            agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
3673        ))
3674        .unwrap();
3675
3676    let output = task.await.unwrap();
3677    cx.run_until_parked();
3678    let expected = format!(
3679        concat!(
3680            "<skill_content name=\"test-skill\">\n",
3681            "<source>project-local</source>\n",
3682            "<worktree>project</worktree>\n",
3683            "<directory>{}</directory>\n",
3684            "Relative paths in this skill resolve against <directory>.\n",
3685            "\n",
3686            "test body\n",
3687            "</skill_content>\n",
3688        ),
3689        path!("/project/.agents/skills/test-skill"),
3690    );
3691    assert_eq!(output, SkillToolOutput::Found { rendered: expected });
3692
3693    fs.create_dir(Path::new(path!("/project/.agents/skills/test-2")))
3694        .await
3695        .unwrap();
3696    fs.insert_file(
3697        path!("/project/.agents/skills/test-2/SKILL.md"),
3698        "---\nname: test-2\ndescription: test description\n---\ntest body"
3699            .as_bytes()
3700            .into(),
3701    )
3702    .await;
3703
3704    cx.run_until_parked();
3705    cx.update(|cx| connection.refresh_skills_for_project(project, cx));
3706    cx.run_until_parked();
3707
3708    let input2 = SkillToolInput {
3709        name: "test-2".into(),
3710    };
3711    let task = cx.update(|cx| {
3712        skill_tool
3713            .clone()
3714            .run(ToolInput::resolved(input2), event_stream.clone(), cx)
3715    });
3716
3717    let authorization = event_stream_rx.expect_authorization().await;
3718    authorization
3719        .response
3720        .send(acp_thread::SelectedPermissionOutcome::new(
3721            agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
3722            agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
3723        ))
3724        .unwrap();
3725
3726    let output = task.await.unwrap();
3727    let expected2 = format!(
3728        concat!(
3729            "<skill_content name=\"test-2\">\n",
3730            "<source>project-local</source>\n",
3731            "<worktree>project</worktree>\n",
3732            "<directory>{}</directory>\n",
3733            "Relative paths in this skill resolve against <directory>.\n",
3734            "\n",
3735            "test body\n",
3736            "</skill_content>\n",
3737        ),
3738        path!("/project/.agents/skills/test-2"),
3739    );
3740    assert_eq!(
3741        output,
3742        SkillToolOutput::Found {
3743            rendered: expected2
3744        }
3745    );
3746}
3747
3748#[gpui::test]
3749async fn test_remote_external_agent_server(
3750    cx: &mut TestAppContext,
3751    server_cx: &mut TestAppContext,
3752) {
3753    let fs = FakeFs::new(server_cx.executor());
3754    fs.insert_tree(path!("/project"), json!({})).await;
3755
3756    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3757    project
3758        .update(cx, |project, cx| {
3759            project.find_or_create_worktree(path!("/project"), true, cx)
3760        })
3761        .await
3762        .unwrap();
3763    let names = project.update(cx, |project, cx| {
3764        project
3765            .agent_server_store()
3766            .read(cx)
3767            .external_agents()
3768            .map(|name| name.to_string())
3769            .collect::<Vec<_>>()
3770    });
3771    pretty_assertions::assert_eq!(names, Vec::<String>::new());
3772    server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
3773        settings_store
3774            .set_server_settings(
3775                &json!({
3776                    "agent_servers": {
3777                        "foo": {
3778                            "type": "custom",
3779                            "command": "foo-cli",
3780                            "args": ["--flag"],
3781                            "env": {
3782                                "VAR": "val"
3783                            }
3784                        }
3785                    }
3786                })
3787                .to_string(),
3788                cx,
3789            )
3790            .unwrap();
3791    });
3792    server_cx.run_until_parked();
3793    cx.run_until_parked();
3794    let names = project.update(cx, |project, cx| {
3795        project
3796            .agent_server_store()
3797            .read(cx)
3798            .external_agents()
3799            .map(|name| name.to_string())
3800            .collect::<Vec<_>>()
3801    });
3802    pretty_assertions::assert_eq!(names, ["foo"]);
3803    let command = project
3804        .update(cx, |project, cx| {
3805            project.agent_server_store().update(cx, |store, cx| {
3806                store
3807                    .get_external_agent(&"foo".into())
3808                    .unwrap()
3809                    .get_command(
3810                        vec![],
3811                        HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
3812                        &mut cx.to_async(),
3813                    )
3814            })
3815        })
3816        .await
3817        .unwrap();
3818    assert_eq!(
3819        command,
3820        AgentServerCommand {
3821            path: "foo-cli".into(),
3822            args: vec!["--flag".into()],
3823            env: Some(HashMap::from_iter([
3824                ("NO_BROWSER".into(), "1".into()),
3825                ("VAR".into(), "val".into()),
3826                ("OTHER_VAR".into(), "other-val".into())
3827            ]))
3828        }
3829    );
3830}
3831
3832#[gpui::test]
3833async fn test_remote_apply_code_action_skips_unadvertised_command(
3834    cx: &mut TestAppContext,
3835    server_cx: &mut TestAppContext,
3836) {
3837    let fs = FakeFs::new(server_cx.executor());
3838    fs.insert_tree(
3839        path!("/code"),
3840        json!({
3841            "project1": {
3842                ".git": {},
3843                "README.md": "# project 1",
3844                "src": {
3845                    "lib.rs": "fn one() -> usize { 1 }"
3846                }
3847            },
3848        }),
3849    )
3850    .await;
3851
3852    let (project, headless) = init_test(&fs, cx, server_cx).await;
3853
3854    fs.insert_tree(
3855        path!("/code/project1/.zed"),
3856        json!({
3857            "settings.json": r#"
3858          {
3859            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
3860            "lsp": {
3861              "rust-analyzer": {
3862                "binary": {
3863                  "path": "~/.cargo/bin/rust-analyzer"
3864                }
3865              }
3866            }
3867          }"#
3868        }),
3869    )
3870    .await;
3871
3872    cx.update_entity(&project, |project, _| {
3873        project.languages().register_test_language(LanguageConfig {
3874            name: "Rust".into(),
3875            matcher: (LanguageMatcher {
3876                path_suffixes: vec!["rs".into()],
3877                ..Default::default()
3878            })
3879            .into(),
3880            ..Default::default()
3881        });
3882        project.languages().register_fake_lsp_adapter(
3883            "Rust",
3884            FakeLspAdapter {
3885                name: "rust-analyzer",
3886                ..Default::default()
3887            },
3888        )
3889    });
3890
3891    // Register the fake LSP with an empty execute_command_provider and a handler that panics
3892    // if it is ever reached: commands not advertised by the server must be rejected by
3893    // `apply_code_action` before dispatching to the language server.
3894    let mut fake_lsp = server_cx.update(|cx| {
3895        headless.read(cx).languages.register_fake_lsp_server(
3896            LanguageServerName("rust-analyzer".into()),
3897            lsp::ServerCapabilities {
3898                execute_command_provider: Some(lsp::ExecuteCommandOptions {
3899                    commands: Vec::new(),
3900                    ..Default::default()
3901                }),
3902                ..Default::default()
3903            },
3904            Some(Box::new(|fake| {
3905                fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
3906                    |params, _| async move {
3907                        panic!(
3908                            "Unadvertised command {} must not reach the language server",
3909                            params.command
3910                        );
3911                    },
3912                );
3913            })),
3914        )
3915    });
3916
3917    cx.run_until_parked();
3918
3919    let worktree_id = project
3920        .update(cx, |project, cx| {
3921            project.find_or_create_worktree(path!("/code/project1"), true, cx)
3922        })
3923        .await
3924        .unwrap()
3925        .0
3926        .read_with(cx, |worktree, _| worktree.id());
3927
3928    cx.run_until_parked();
3929
3930    let (buffer, _handle) = project
3931        .update(cx, |project, cx| {
3932            project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
3933        })
3934        .await
3935        .unwrap();
3936
3937    cx.run_until_parked();
3938
3939    let _fake_lsp = fake_lsp.next().await.unwrap();
3940
3941    let server_id = server_cx.read(|cx| {
3942        *headless
3943            .read(cx)
3944            .lsp_store
3945            .read(cx)
3946            .as_local()
3947            .unwrap()
3948            .language_servers
3949            .keys()
3950            .next()
3951            .unwrap()
3952    });
3953    let buffer_id = cx.read(|cx| buffer.read(cx).remote_id());
3954
3955    let action = project::CodeAction {
3956        server_id,
3957        range: language::Anchor::min_min_range_for_buffer(buffer_id),
3958        lsp_action: project::LspAction::Command(lsp::Command {
3959            title: "\u{25b6}\u{fe0e} Run Tests".into(),
3960            command: "rust-analyzer.runSingle".into(),
3961            arguments: Some(vec![json!({"label": "test-mod tests"})]),
3962        }),
3963        resolved: true,
3964    };
3965
3966    let transaction = project
3967        .update(cx, |project, cx| {
3968            project.apply_code_action(buffer.clone(), action, true, cx)
3969        })
3970        .await
3971        .expect("Unadvertised command must not be forwarded to executeCommand");
3972    assert_eq!(transaction.0.len(), 0);
3973}
3974
3975#[gpui::test]
3976async fn test_remote_restore_unstaged_hunk_clears_diff(
3977    cx: &mut TestAppContext,
3978    server_cx: &mut TestAppContext,
3979) {
3980    cx.update(|cx| {
3981        let settings_store = SettingsStore::test(cx);
3982        cx.set_global(settings_store);
3983        theme_settings::init(theme::LoadThemes::JustBase, cx);
3984        release_channel::init(semver::Version::new(0, 0, 0), cx);
3985        editor::init(cx);
3986    });
3987
3988    use editor::Editor;
3989    use gpui::VisualContext;
3990
3991    let base_text = "
3992        fn one() -> usize {
3993            1
3994        }
3995    "
3996    .unindent();
3997    let modified_text = "
3998        fn one() -> usize {
3999            100
4000        }
4001    "
4002    .unindent();
4003
4004    let fs = FakeFs::new(server_cx.executor());
4005    fs.insert_tree(
4006        path!("/code"),
4007        json!({
4008            "project1": {
4009                ".git": {},
4010                "src": {
4011                    "lib.rs": modified_text
4012                },
4013            },
4014        }),
4015    )
4016    .await;
4017    fs.set_index_for_repo(
4018        Path::new(path!("/code/project1/.git")),
4019        &[("src/lib.rs", base_text.clone())],
4020    );
4021    fs.set_head_for_repo(
4022        Path::new(path!("/code/project1/.git")),
4023        &[("src/lib.rs", base_text.clone())],
4024        "deadbeef",
4025    );
4026
4027    let (project, _headless) = init_test(&fs, cx, server_cx).await;
4028    let worktree_id = {
4029        let (worktree, _) = project
4030            .update(cx, |project, cx| {
4031                project.find_or_create_worktree(path!("/code/project1"), true, cx)
4032            })
4033            .await
4034            .unwrap();
4035        cx.update(|cx| worktree.read(cx).id())
4036    };
4037    cx.executor().run_until_parked();
4038
4039    let buffer = project
4040        .update(cx, |project, cx| {
4041            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
4042        })
4043        .await
4044        .unwrap();
4045
4046    let cx = cx.add_empty_window();
4047    let editor = cx.new_window_entity(|window, cx| {
4048        Editor::for_buffer(buffer, Some(project.clone()), window, cx)
4049    });
4050    cx.executor().run_until_parked();
4051
4052    editor.update_in(cx, |editor, window, cx| {
4053        let snapshot = editor.snapshot(window, cx);
4054        let hunks: Vec<_> = editor
4055            .diff_hunks_in_ranges(
4056                &[editor::Anchor::Min..editor::Anchor::Max],
4057                &snapshot.buffer_snapshot(),
4058            )
4059            .collect();
4060        assert!(!hunks.is_empty(), "should have diff hunks before restore");
4061    });
4062
4063    cx.update_window_entity(&editor, |editor, window, cx| {
4064        editor.select_all(&editor::actions::SelectAll, window, cx);
4065        editor.git_restore(&git::Restore, window, cx);
4066    });
4067    cx.executor().run_until_parked();
4068
4069    editor.update_in(cx, |editor, _window, cx| {
4070        let snapshot = editor.buffer().read(cx).snapshot(cx);
4071        assert_eq!(
4072            snapshot.text(),
4073            base_text,
4074            "buffer text should match base after restoring all hunks"
4075        );
4076
4077        let hunks: Vec<_> = editor
4078            .diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
4079            .collect();
4080        assert!(hunks.is_empty(), "should have no diff hunks after restore");
4081    });
4082}
4083
4084#[gpui::test]
4085async fn test_remote_load_commit_template(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
4086    let fs = FakeFs::new(server_cx.executor());
4087
4088    fs.insert_tree(
4089        path!("/root"),
4090        json!({
4091            "project": {
4092                ".git": {},
4093                "README.md": "# Project's README"
4094            }
4095        }),
4096    )
4097    .await;
4098
4099    fs.with_git_state(Path::new(path!("/root/project/.git")), false, |state| {
4100        state.commit_template = Some(GitCommitTemplate {
4101            template: "chore: commit template".to_string(),
4102        })
4103    })
4104    .expect("Should successfully update repository state");
4105
4106    let (project, _) = init_test(&fs, cx, server_cx).await;
4107    project
4108        .update(cx, |project, cx| {
4109            project.find_or_create_worktree(path!("/root/project"), true, cx)
4110        })
4111        .await
4112        .unwrap();
4113    cx.run_until_parked();
4114
4115    let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
4116    let commit_template = repository
4117        .update(cx, |repository, _| repository.load_commit_template_text())
4118        .await
4119        .unwrap()
4120        .unwrap()
4121        .expect("Loading commit template in remote should work");
4122
4123    assert_eq!(commit_template.template, "chore: commit template");
4124}
4125
4126#[gpui::test]
4127async fn test_remote_delete_project_entry_with_trash(
4128    cx: &mut TestAppContext,
4129    server_cx: &mut TestAppContext,
4130) {
4131    let fs = FakeFs::new(server_cx.executor());
4132
4133    fs.insert_tree(
4134        path!("/root"),
4135        json!({
4136            "zed": {
4137                "file_a.txt": "File A"
4138            }
4139        }),
4140    )
4141    .await;
4142
4143    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
4144    let (worktree, _path) = project
4145        .update(cx, |project, cx| {
4146            project.find_or_create_worktree(path!("/root/zed"), true, cx)
4147        })
4148        .await
4149        .unwrap();
4150    cx.run_until_parked();
4151
4152    let entry_id = worktree.update(cx, |worktree, _cx| {
4153        worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
4154    });
4155
4156    let remote_client = project.read_with(cx, |project, _cx| {
4157        project
4158            .remote_client()
4159            .expect("project should have a remote client")
4160    });
4161
4162    let proto_client =
4163        remote_client.read_with(cx, |remote_client, _cx| remote_client.proto_client());
4164
4165    proto_client
4166        .request(proto::DeleteProjectEntry {
4167            project_id: proto::REMOTE_SERVER_PROJECT_ID,
4168            entry_id: entry_id.to_proto(),
4169            #[allow(deprecated)]
4170            use_trash: true,
4171        })
4172        .await
4173        .unwrap();
4174
4175    assert_eq!(
4176        fs.trashed_paths(),
4177        vec![PathBuf::from(path!("/root/zed/file_a.txt"))]
4178    );
4179}
4180
4181#[gpui::test]
4182async fn test_remote_trash_restore(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
4183    let fs = FakeFs::new(server_cx.executor());
4184
4185    fs.insert_tree(
4186        path!("/root"),
4187        json!({
4188            "zed": {
4189                "file_a.txt": "File A"
4190            }
4191        }),
4192    )
4193    .await;
4194
4195    let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
4196    let (worktree, _path) = project
4197        .update(cx, |project, cx| {
4198            project.find_or_create_worktree(path!("/root/zed"), true, cx)
4199        })
4200        .await
4201        .unwrap();
4202    cx.run_until_parked();
4203
4204    let worktree_id = worktree.update(cx, |worktree, _cx| worktree.id());
4205    let entry_id = worktree.update(cx, |worktree, _cx| {
4206        worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
4207    });
4208
4209    let trash_id = project
4210        .update(cx, |project, cx| project.trash_entry(entry_id, cx))
4211        .unwrap()
4212        .await
4213        .unwrap();
4214    cx.run_until_parked();
4215
4216    worktree.update(cx, |worktree, _cx| {
4217        assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_none());
4218    });
4219
4220    project
4221        .update(cx, |project, cx| {
4222            project.restore_entry(worktree_id, trash_id, cx)
4223        })
4224        .await
4225        .unwrap();
4226    cx.run_until_parked();
4227
4228    worktree.update(cx, |worktree, _cx| {
4229        assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_some());
4230    });
4231}
4232
4233pub async fn init_test(
4234    server_fs: &Arc<FakeFs>,
4235    cx: &mut TestAppContext,
4236    server_cx: &mut TestAppContext,
4237) -> (Entity<Project>, Entity<HeadlessProject>) {
4238    let server_fs = server_fs.clone();
4239    cx.update(|cx| {
4240        release_channel::init(semver::Version::new(0, 0, 0), cx);
4241    });
4242    server_cx.update(|cx| {
4243        release_channel::init(semver::Version::new(0, 0, 0), cx);
4244    });
4245    init_logger();
4246
4247    let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
4248    let http_client = Arc::new(BlockedHttpClient);
4249    let node_runtime = NodeRuntime::unavailable();
4250    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
4251    let proxy = Arc::new(ExtensionHostProxy::new());
4252    server_cx.update(HeadlessProject::init);
4253    let headless = server_cx.new(|cx| {
4254        HeadlessProject::new(
4255            crate::HeadlessAppState {
4256                session: ssh_server_client,
4257                fs: server_fs.clone(),
4258                http_client,
4259                node_runtime,
4260                languages,
4261                extension_host_proxy: proxy,
4262                startup_time: std::time::Instant::now(),
4263            },
4264            false,
4265            cx,
4266        )
4267    });
4268
4269    let ssh = RemoteClient::connect_mock(opts, cx).await;
4270    let project = build_project(ssh, cx);
4271    project
4272        .update(cx, {
4273            let headless = headless.clone();
4274            |_, cx| cx.on_release(|_, _| drop(headless))
4275        })
4276        .detach();
4277    (project, headless)
4278}
4279
4280fn init_logger() {
4281    zlog::init_test();
4282}
4283
4284fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
4285    cx.update(|cx| {
4286        if !cx.has_global::<SettingsStore>() {
4287            let settings_store = SettingsStore::test(cx);
4288            cx.set_global(settings_store);
4289        }
4290    });
4291
4292    let client = cx.update(|cx| {
4293        Client::new(
4294            Arc::new(FakeSystemClock::new()),
4295            FakeHttpClient::with_404_response(),
4296            cx,
4297        )
4298    });
4299
4300    let node = NodeRuntime::unavailable();
4301    let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
4302    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
4303    let fs = FakeFs::new(cx.executor());
4304
4305    cx.update(|cx| {
4306        Project::init(&client, cx);
4307    });
4308
4309    cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
4310}
4311
Served at tenant.openagents/omega Member data and write actions are omitted.