1use crate::{AgentTool, Thread, ToolCallEventStream};
2use acp_thread::Diff;
3use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
4use anyhow::{Context as _, Result, anyhow};
5use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
6use cloud_llm_client::CompletionIntent;
7use collections::HashSet;
8use gpui::{App, AppContext, AsyncApp, Entity, Task};
9use indoc::formatdoc;
10use language::ToPoint;
11use language::language_settings::{self, FormatOnSave};
12use language_model::LanguageModelToolResultContent;
13use paths;
14use project::lsp_store::{FormatTrigger, LspFormatTarget};
15use project::{Project, ProjectPath};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use settings::Settings;
19use smol::stream::StreamExt as _;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22use ui::SharedString;
23use util::ResultExt;
24
25const DEFAULT_UI_TEXT: &str = "Editing file";
26
27/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
28///
29/// Before using this tool:
30///
31/// 1. Use the `read_file` tool to understand the file's contents and context
32///
33/// 2. Verify the directory path is correct (only applicable when creating new files):
34/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
35#[derive(Debug, Serialize, Deserialize, JsonSchema)]
36pub struct EditFileToolInput {
37 /// A one-line, user-friendly markdown description of the edit. This will be
38 /// shown in the UI and also passed to another model to perform the edit.
39 ///
40 /// Be terse, but also descriptive in what you want to achieve with this
41 /// edit. Avoid generic instructions.
42 ///
43 /// NEVER mention the file path in this description.
44 ///
45 /// <example>Fix API endpoint URLs</example>
46 /// <example>Update copyright year in `page_footer`</example>
47 ///
48 /// Make sure to include this field before all the others in the input object
49 /// so that we can display it immediately.
50 pub display_description: String,
51
52 /// The full path of the file to create or modify in the project.
53 ///
54 /// WARNING: When specifying which file path need changing, you MUST
55 /// start each path with one of the project's root directories.
56 ///
57 /// The following examples assume we have two root directories in the project:
58 /// - /a/b/backend
59 /// - /c/d/frontend
60 ///
61 /// <example>
62 /// `backend/src/main.rs`
63 ///
64 /// Notice how the file path starts with `backend`. Without that, the path
65 /// would be ambiguous and the call would fail!
66 /// </example>
67 ///
68 /// <example>
69 /// `frontend/db.js`
70 /// </example>
71 pub path: PathBuf,
72
73 /// The mode of operation on the file. Possible values:
74 /// - 'edit': Make granular edits to an existing file.
75 /// - 'create': Create a new file if it doesn't exist.
76 /// - 'overwrite': Replace the entire contents of an existing file.
77 ///
78 /// When a file already exists or you just created it, prefer editing
79 /// it as opposed to recreating it from scratch.
80 pub mode: EditFileMode,
81}
82
83#[derive(Debug, Serialize, Deserialize, JsonSchema)]
84struct EditFileToolPartialInput {
85 #[serde(default)]
86 path: String,
87 #[serde(default)]
88 display_description: String,
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "lowercase")]
93pub enum EditFileMode {
94 Edit,
95 Create,
96 Overwrite,
97}
98
99#[derive(Debug, Serialize, Deserialize)]
100pub struct EditFileToolOutput {
101 input_path: PathBuf,
102 project_path: PathBuf,
103 new_text: String,
104 old_text: Arc<String>,
105 diff: String,
106 edit_agent_output: EditAgentOutput,
107}
108
109impl From<EditFileToolOutput> for LanguageModelToolResultContent {
110 fn from(output: EditFileToolOutput) -> Self {
111 if output.diff.is_empty() {
112 "No edits were made.".into()
113 } else {
114 format!(
115 "Edited {}:\n\n```diff\n{}\n```",
116 output.input_path.display(),
117 output.diff
118 )
119 .into()
120 }
121 }
122}
123
124pub struct EditFileTool {
125 thread: Entity<Thread>,
126}
127
128impl EditFileTool {
129 pub fn new(thread: Entity<Thread>) -> Self {
130 Self { thread }
131 }
132
133 fn authorize(
134 &self,
135 input: &EditFileToolInput,
136 event_stream: &ToolCallEventStream,
137 cx: &mut App,
138 ) -> Task<Result<()>> {
139 if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
140 return Task::ready(Ok(()));
141 }
142
143 // If any path component matches the local settings folder, then this could affect
144 // the editor in ways beyond the project source, so prompt.
145 let local_settings_folder = paths::local_settings_folder_relative_path();
146 let path = Path::new(&input.path);
147 if path
148 .components()
149 .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
150 {
151 return event_stream.authorize(
152 format!("{} (local settings)", input.display_description),
153 cx,
154 );
155 }
156
157 // It's also possible that the global config dir is configured to be inside the project,
158 // so check for that edge case too.
159 if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
160 if canonical_path.starts_with(paths::config_dir()) {
161 return event_stream.authorize(
162 format!("{} (global settings)", input.display_description),
163 cx,
164 );
165 }
166 }
167
168 // Check if path is inside the global config directory
169 // First check if it's already inside project - if not, try to canonicalize
170 let thread = self.thread.read(cx);
171 let project_path = thread.project().read(cx).find_project_path(&input.path, cx);
172
173 // If the path is inside the project, and it's not one of the above edge cases,
174 // then no confirmation is necessary. Otherwise, confirmation is necessary.
175 if project_path.is_some() {
176 Task::ready(Ok(()))
177 } else {
178 event_stream.authorize(&input.display_description, cx)
179 }
180 }
181}
182
183impl AgentTool for EditFileTool {
184 type Input = EditFileToolInput;
185 type Output = EditFileToolOutput;
186
187 fn name(&self) -> SharedString {
188 "edit_file".into()
189 }
190
191 fn kind(&self) -> acp::ToolKind {
192 acp::ToolKind::Edit
193 }
194
195 fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
196 match input {
197 Ok(input) => input.display_description.into(),
198 Err(raw_input) => {
199 if let Some(input) =
200 serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
201 {
202 let description = input.display_description.trim();
203 if !description.is_empty() {
204 return description.to_string().into();
205 }
206
207 let path = input.path.trim().to_string();
208 if !path.is_empty() {
209 return path.into();
210 }
211 }
212
213 DEFAULT_UI_TEXT.into()
214 }
215 }
216 }
217
218 fn run(
219 self: Arc<Self>,
220 input: Self::Input,
221 event_stream: ToolCallEventStream,
222 cx: &mut App,
223 ) -> Task<Result<Self::Output>> {
224 let project = self.thread.read(cx).project().clone();
225 let project_path = match resolve_path(&input, project.clone(), cx) {
226 Ok(path) => path,
227 Err(err) => return Task::ready(Err(anyhow!(err))),
228 };
229 let abs_path = project.read(cx).absolute_path(&project_path, cx);
230 if let Some(abs_path) = abs_path.clone() {
231 event_stream.update_fields(ToolCallUpdateFields {
232 locations: Some(vec![acp::ToolCallLocation {
233 path: abs_path,
234 line: None,
235 }]),
236 ..Default::default()
237 });
238 }
239
240 let Some(request) = self.thread.update(cx, |thread, cx| {
241 thread
242 .build_completion_request(CompletionIntent::ToolResults, cx)
243 .ok()
244 }) else {
245 return Task::ready(Err(anyhow!("Failed to build completion request")));
246 };
247 let thread = self.thread.read(cx);
248 let Some(model) = thread.model().cloned() else {
249 return Task::ready(Err(anyhow!("No language model configured")));
250 };
251 let action_log = thread.action_log().clone();
252
253 let authorize = self.authorize(&input, &event_stream, cx);
254 cx.spawn(async move |cx: &mut AsyncApp| {
255 authorize.await?;
256
257 let edit_format = EditFormat::from_model(model.clone())?;
258 let edit_agent = EditAgent::new(
259 model,
260 project.clone(),
261 action_log.clone(),
262 // TODO: move edit agent to this crate so we can use our templates
263 assistant_tools::templates::Templates::new(),
264 edit_format,
265 );
266
267 let buffer = project
268 .update(cx, |project, cx| {
269 project.open_buffer(project_path.clone(), cx)
270 })?
271 .await?;
272
273 let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
274 event_stream.update_diff(diff.clone());
275
276 let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
277 let old_text = cx
278 .background_spawn({
279 let old_snapshot = old_snapshot.clone();
280 async move { Arc::new(old_snapshot.text()) }
281 })
282 .await;
283
284
285 let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
286 edit_agent.edit(
287 buffer.clone(),
288 input.display_description.clone(),
289 &request,
290 cx,
291 )
292 } else {
293 edit_agent.overwrite(
294 buffer.clone(),
295 input.display_description.clone(),
296 &request,
297 cx,
298 )
299 };
300
301 let mut hallucinated_old_text = false;
302 let mut ambiguous_ranges = Vec::new();
303 let mut emitted_location = false;
304 while let Some(event) = events.next().await {
305 match event {
306 EditAgentOutputEvent::Edited(range) => {
307 if !emitted_location {
308 let line = buffer.update(cx, |buffer, _cx| {
309 range.start.to_point(&buffer.snapshot()).row
310 }).ok();
311 if let Some(abs_path) = abs_path.clone() {
312 event_stream.update_fields(ToolCallUpdateFields {
313 locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
314 ..Default::default()
315 });
316 }
317 emitted_location = true;
318 }
319 },
320 EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
321 EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
322 EditAgentOutputEvent::ResolvingEditRange(range) => {
323 diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
324 // if !emitted_location {
325 // let line = buffer.update(cx, |buffer, _cx| {
326 // range.start.to_point(&buffer.snapshot()).row
327 // }).ok();
328 // if let Some(abs_path) = abs_path.clone() {
329 // event_stream.update_fields(ToolCallUpdateFields {
330 // locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
331 // ..Default::default()
332 // });
333 // }
334 // }
335 }
336 }
337 }
338
339 // If format_on_save is enabled, format the buffer
340 let format_on_save_enabled = buffer
341 .read_with(cx, |buffer, cx| {
342 let settings = language_settings::language_settings(
343 buffer.language().map(|l| l.name()),
344 buffer.file(),
345 cx,
346 );
347 settings.format_on_save != FormatOnSave::Off
348 })
349 .unwrap_or(false);
350
351 let edit_agent_output = output.await?;
352
353 if format_on_save_enabled {
354 action_log.update(cx, |log, cx| {
355 log.buffer_edited(buffer.clone(), cx);
356 })?;
357
358 let format_task = project.update(cx, |project, cx| {
359 project.format(
360 HashSet::from_iter([buffer.clone()]),
361 LspFormatTarget::Buffers,
362 false, // Don't push to history since the tool did it.
363 FormatTrigger::Save,
364 cx,
365 )
366 })?;
367 format_task.await.log_err();
368 }
369
370 project
371 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
372 .await?;
373
374 action_log.update(cx, |log, cx| {
375 log.buffer_edited(buffer.clone(), cx);
376 })?;
377
378 let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
379 let (new_text, unified_diff) = cx
380 .background_spawn({
381 let new_snapshot = new_snapshot.clone();
382 let old_text = old_text.clone();
383 async move {
384 let new_text = new_snapshot.text();
385 let diff = language::unified_diff(&old_text, &new_text);
386 (new_text, diff)
387 }
388 })
389 .await;
390
391 diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
392
393 let input_path = input.path.display();
394 if unified_diff.is_empty() {
395 anyhow::ensure!(
396 !hallucinated_old_text,
397 formatdoc! {"
398 Some edits were produced but none of them could be applied.
399 Read the relevant sections of {input_path} again so that
400 I can perform the requested edits.
401 "}
402 );
403 anyhow::ensure!(
404 ambiguous_ranges.is_empty(),
405 {
406 let line_numbers = ambiguous_ranges
407 .iter()
408 .map(|range| range.start.to_string())
409 .collect::<Vec<_>>()
410 .join(", ");
411 formatdoc! {"
412 <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
413 relevant sections of {input_path} again and extend <old_text> so
414 that I can perform the requested edits.
415 "}
416 }
417 );
418 }
419
420 Ok(EditFileToolOutput {
421 input_path: input.path,
422 project_path: project_path.path.to_path_buf(),
423 new_text: new_text.clone(),
424 old_text,
425 diff: unified_diff,
426 edit_agent_output,
427 })
428 })
429 }
430}
431
432/// Validate that the file path is valid, meaning:
433///
434/// - For `edit` and `overwrite`, the path must point to an existing file.
435/// - For `create`, the file must not already exist, but it's parent dir must exist.
436fn resolve_path(
437 input: &EditFileToolInput,
438 project: Entity<Project>,
439 cx: &mut App,
440) -> Result<ProjectPath> {
441 let project = project.read(cx);
442
443 match input.mode {
444 EditFileMode::Edit | EditFileMode::Overwrite => {
445 let path = project
446 .find_project_path(&input.path, cx)
447 .context("Can't edit file: path not found")?;
448
449 let entry = project
450 .entry_for_path(&path, cx)
451 .context("Can't edit file: path not found")?;
452
453 anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
454 Ok(path)
455 }
456
457 EditFileMode::Create => {
458 if let Some(path) = project.find_project_path(&input.path, cx) {
459 anyhow::ensure!(
460 project.entry_for_path(&path, cx).is_none(),
461 "Can't create file: file already exists"
462 );
463 }
464
465 let parent_path = input
466 .path
467 .parent()
468 .context("Can't create file: incorrect path")?;
469
470 let parent_project_path = project.find_project_path(&parent_path, cx);
471
472 let parent_entry = parent_project_path
473 .as_ref()
474 .and_then(|path| project.entry_for_path(&path, cx))
475 .context("Can't create file: parent directory doesn't exist")?;
476
477 anyhow::ensure!(
478 parent_entry.is_dir(),
479 "Can't create file: parent is not a directory"
480 );
481
482 let file_name = input
483 .path
484 .file_name()
485 .context("Can't create file: invalid filename")?;
486
487 let new_file_path = parent_project_path.map(|parent| ProjectPath {
488 path: Arc::from(parent.path.join(file_name)),
489 ..parent
490 });
491
492 new_file_path.context("Can't create file")
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::{ContextServerRegistry, Templates};
501 use action_log::ActionLog;
502 use client::TelemetrySettings;
503 use fs::Fs;
504 use gpui::{TestAppContext, UpdateGlobal};
505 use language_model::fake_provider::FakeLanguageModel;
506 use serde_json::json;
507 use settings::SettingsStore;
508 use std::rc::Rc;
509 use util::path;
510
511 #[gpui::test]
512 async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
513 init_test(cx);
514
515 let fs = project::FakeFs::new(cx.executor());
516 fs.insert_tree("/root", json!({})).await;
517 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
518 let action_log = cx.new(|_| ActionLog::new(project.clone()));
519 let context_server_registry =
520 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
521 let model = Arc::new(FakeLanguageModel::default());
522 let thread = cx.new(|cx| {
523 Thread::new(
524 project,
525 Rc::default(),
526 context_server_registry,
527 action_log,
528 Templates::new(),
529 Some(model),
530 cx,
531 )
532 });
533 let result = cx
534 .update(|cx| {
535 let input = EditFileToolInput {
536 display_description: "Some edit".into(),
537 path: "root/nonexistent_file.txt".into(),
538 mode: EditFileMode::Edit,
539 };
540 Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
541 })
542 .await;
543 assert_eq!(
544 result.unwrap_err().to_string(),
545 "Can't edit file: path not found"
546 );
547 }
548
549 #[gpui::test]
550 async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
551 let mode = &EditFileMode::Create;
552
553 let result = test_resolve_path(mode, "root/new.txt", cx);
554 assert_resolved_path_eq(result.await, "new.txt");
555
556 let result = test_resolve_path(mode, "new.txt", cx);
557 assert_resolved_path_eq(result.await, "new.txt");
558
559 let result = test_resolve_path(mode, "dir/new.txt", cx);
560 assert_resolved_path_eq(result.await, "dir/new.txt");
561
562 let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
563 assert_eq!(
564 result.await.unwrap_err().to_string(),
565 "Can't create file: file already exists"
566 );
567
568 let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
569 assert_eq!(
570 result.await.unwrap_err().to_string(),
571 "Can't create file: parent directory doesn't exist"
572 );
573 }
574
575 #[gpui::test]
576 async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
577 let mode = &EditFileMode::Edit;
578
579 let path_with_root = "root/dir/subdir/existing.txt";
580 let path_without_root = "dir/subdir/existing.txt";
581 let result = test_resolve_path(mode, path_with_root, cx);
582 assert_resolved_path_eq(result.await, path_without_root);
583
584 let result = test_resolve_path(mode, path_without_root, cx);
585 assert_resolved_path_eq(result.await, path_without_root);
586
587 let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
588 assert_eq!(
589 result.await.unwrap_err().to_string(),
590 "Can't edit file: path not found"
591 );
592
593 let result = test_resolve_path(mode, "root/dir", cx);
594 assert_eq!(
595 result.await.unwrap_err().to_string(),
596 "Can't edit file: path is a directory"
597 );
598 }
599
600 async fn test_resolve_path(
601 mode: &EditFileMode,
602 path: &str,
603 cx: &mut TestAppContext,
604 ) -> anyhow::Result<ProjectPath> {
605 init_test(cx);
606
607 let fs = project::FakeFs::new(cx.executor());
608 fs.insert_tree(
609 "/root",
610 json!({
611 "dir": {
612 "subdir": {
613 "existing.txt": "hello"
614 }
615 }
616 }),
617 )
618 .await;
619 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
620
621 let input = EditFileToolInput {
622 display_description: "Some edit".into(),
623 path: path.into(),
624 mode: mode.clone(),
625 };
626
627 let result = cx.update(|cx| resolve_path(&input, project, cx));
628 result
629 }
630
631 fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
632 let actual = path
633 .expect("Should return valid path")
634 .path
635 .to_str()
636 .unwrap()
637 .replace("\\", "/"); // Naive Windows paths normalization
638 assert_eq!(actual, expected);
639 }
640
641 #[gpui::test]
642 async fn test_format_on_save(cx: &mut TestAppContext) {
643 init_test(cx);
644
645 let fs = project::FakeFs::new(cx.executor());
646 fs.insert_tree("/root", json!({"src": {}})).await;
647
648 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
649
650 // Set up a Rust language with LSP formatting support
651 let rust_language = Arc::new(language::Language::new(
652 language::LanguageConfig {
653 name: "Rust".into(),
654 matcher: language::LanguageMatcher {
655 path_suffixes: vec!["rs".to_string()],
656 ..Default::default()
657 },
658 ..Default::default()
659 },
660 None,
661 ));
662
663 // Register the language and fake LSP
664 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
665 language_registry.add(rust_language);
666
667 let mut fake_language_servers = language_registry.register_fake_lsp(
668 "Rust",
669 language::FakeLspAdapter {
670 capabilities: lsp::ServerCapabilities {
671 document_formatting_provider: Some(lsp::OneOf::Left(true)),
672 ..Default::default()
673 },
674 ..Default::default()
675 },
676 );
677
678 // Create the file
679 fs.save(
680 path!("/root/src/main.rs").as_ref(),
681 &"initial content".into(),
682 language::LineEnding::Unix,
683 )
684 .await
685 .unwrap();
686
687 // Open the buffer to trigger LSP initialization
688 let buffer = project
689 .update(cx, |project, cx| {
690 project.open_local_buffer(path!("/root/src/main.rs"), cx)
691 })
692 .await
693 .unwrap();
694
695 // Register the buffer with language servers
696 let _handle = project.update(cx, |project, cx| {
697 project.register_buffer_with_language_servers(&buffer, cx)
698 });
699
700 const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
701 const FORMATTED_CONTENT: &str =
702 "This file was formatted by the fake formatter in the test.\n";
703
704 // Get the fake language server and set up formatting handler
705 let fake_language_server = fake_language_servers.next().await.unwrap();
706 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
707 |_, _| async move {
708 Ok(Some(vec![lsp::TextEdit {
709 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
710 new_text: FORMATTED_CONTENT.to_string(),
711 }]))
712 }
713 });
714
715 let action_log = cx.new(|_| ActionLog::new(project.clone()));
716 let context_server_registry =
717 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
718 let model = Arc::new(FakeLanguageModel::default());
719 let thread = cx.new(|cx| {
720 Thread::new(
721 project,
722 Rc::default(),
723 context_server_registry,
724 action_log.clone(),
725 Templates::new(),
726 Some(model.clone()),
727 cx,
728 )
729 });
730
731 // First, test with format_on_save enabled
732 cx.update(|cx| {
733 SettingsStore::update_global(cx, |store, cx| {
734 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
735 cx,
736 |settings| {
737 settings.defaults.format_on_save = Some(FormatOnSave::On);
738 settings.defaults.formatter =
739 Some(language::language_settings::SelectedFormatter::Auto);
740 },
741 );
742 });
743 });
744
745 // Have the model stream unformatted content
746 let edit_result = {
747 let edit_task = cx.update(|cx| {
748 let input = EditFileToolInput {
749 display_description: "Create main function".into(),
750 path: "root/src/main.rs".into(),
751 mode: EditFileMode::Overwrite,
752 };
753 Arc::new(EditFileTool {
754 thread: thread.clone(),
755 })
756 .run(input, ToolCallEventStream::test().0, cx)
757 });
758
759 // Stream the unformatted content
760 cx.executor().run_until_parked();
761 model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
762 model.end_last_completion_stream();
763
764 edit_task.await
765 };
766 assert!(edit_result.is_ok());
767
768 // Wait for any async operations (e.g. formatting) to complete
769 cx.executor().run_until_parked();
770
771 // Read the file to verify it was formatted automatically
772 let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
773 assert_eq!(
774 // Ignore carriage returns on Windows
775 new_content.replace("\r\n", "\n"),
776 FORMATTED_CONTENT,
777 "Code should be formatted when format_on_save is enabled"
778 );
779
780 let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
781
782 assert_eq!(
783 stale_buffer_count, 0,
784 "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
785 This causes the agent to think the file was modified externally when it was just formatted.",
786 stale_buffer_count
787 );
788
789 // Next, test with format_on_save disabled
790 cx.update(|cx| {
791 SettingsStore::update_global(cx, |store, cx| {
792 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
793 cx,
794 |settings| {
795 settings.defaults.format_on_save = Some(FormatOnSave::Off);
796 },
797 );
798 });
799 });
800
801 // Stream unformatted edits again
802 let edit_result = {
803 let edit_task = cx.update(|cx| {
804 let input = EditFileToolInput {
805 display_description: "Update main function".into(),
806 path: "root/src/main.rs".into(),
807 mode: EditFileMode::Overwrite,
808 };
809 Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
810 });
811
812 // Stream the unformatted content
813 cx.executor().run_until_parked();
814 model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
815 model.end_last_completion_stream();
816
817 edit_task.await
818 };
819 assert!(edit_result.is_ok());
820
821 // Wait for any async operations (e.g. formatting) to complete
822 cx.executor().run_until_parked();
823
824 // Verify the file was not formatted
825 let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
826 assert_eq!(
827 // Ignore carriage returns on Windows
828 new_content.replace("\r\n", "\n"),
829 UNFORMATTED_CONTENT,
830 "Code should not be formatted when format_on_save is disabled"
831 );
832 }
833
834 #[gpui::test]
835 async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
836 init_test(cx);
837
838 let fs = project::FakeFs::new(cx.executor());
839 fs.insert_tree("/root", json!({"src": {}})).await;
840
841 // Create a simple file with trailing whitespace
842 fs.save(
843 path!("/root/src/main.rs").as_ref(),
844 &"initial content".into(),
845 language::LineEnding::Unix,
846 )
847 .await
848 .unwrap();
849
850 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
851 let context_server_registry =
852 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
853 let action_log = cx.new(|_| ActionLog::new(project.clone()));
854 let model = Arc::new(FakeLanguageModel::default());
855 let thread = cx.new(|cx| {
856 Thread::new(
857 project,
858 Rc::default(),
859 context_server_registry,
860 action_log.clone(),
861 Templates::new(),
862 Some(model.clone()),
863 cx,
864 )
865 });
866
867 // First, test with remove_trailing_whitespace_on_save enabled
868 cx.update(|cx| {
869 SettingsStore::update_global(cx, |store, cx| {
870 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
871 cx,
872 |settings| {
873 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
874 },
875 );
876 });
877 });
878
879 const CONTENT_WITH_TRAILING_WHITESPACE: &str =
880 "fn main() { \n println!(\"Hello!\"); \n}\n";
881
882 // Have the model stream content that contains trailing whitespace
883 let edit_result = {
884 let edit_task = cx.update(|cx| {
885 let input = EditFileToolInput {
886 display_description: "Create main function".into(),
887 path: "root/src/main.rs".into(),
888 mode: EditFileMode::Overwrite,
889 };
890 Arc::new(EditFileTool {
891 thread: thread.clone(),
892 })
893 .run(input, ToolCallEventStream::test().0, cx)
894 });
895
896 // Stream the content with trailing whitespace
897 cx.executor().run_until_parked();
898 model.send_last_completion_stream_text_chunk(
899 CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
900 );
901 model.end_last_completion_stream();
902
903 edit_task.await
904 };
905 assert!(edit_result.is_ok());
906
907 // Wait for any async operations (e.g. formatting) to complete
908 cx.executor().run_until_parked();
909
910 // Read the file to verify trailing whitespace was removed automatically
911 assert_eq!(
912 // Ignore carriage returns on Windows
913 fs.load(path!("/root/src/main.rs").as_ref())
914 .await
915 .unwrap()
916 .replace("\r\n", "\n"),
917 "fn main() {\n println!(\"Hello!\");\n}\n",
918 "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
919 );
920
921 // Next, test with remove_trailing_whitespace_on_save disabled
922 cx.update(|cx| {
923 SettingsStore::update_global(cx, |store, cx| {
924 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
925 cx,
926 |settings| {
927 settings.defaults.remove_trailing_whitespace_on_save = Some(false);
928 },
929 );
930 });
931 });
932
933 // Stream edits again with trailing whitespace
934 let edit_result = {
935 let edit_task = cx.update(|cx| {
936 let input = EditFileToolInput {
937 display_description: "Update main function".into(),
938 path: "root/src/main.rs".into(),
939 mode: EditFileMode::Overwrite,
940 };
941 Arc::new(EditFileTool {
942 thread: thread.clone(),
943 })
944 .run(input, ToolCallEventStream::test().0, cx)
945 });
946
947 // Stream the content with trailing whitespace
948 cx.executor().run_until_parked();
949 model.send_last_completion_stream_text_chunk(
950 CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
951 );
952 model.end_last_completion_stream();
953
954 edit_task.await
955 };
956 assert!(edit_result.is_ok());
957
958 // Wait for any async operations (e.g. formatting) to complete
959 cx.executor().run_until_parked();
960
961 // Verify the file still has trailing whitespace
962 // Read the file again - it should still have trailing whitespace
963 let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
964 assert_eq!(
965 // Ignore carriage returns on Windows
966 final_content.replace("\r\n", "\n"),
967 CONTENT_WITH_TRAILING_WHITESPACE,
968 "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
969 );
970 }
971
972 #[gpui::test]
973 async fn test_authorize(cx: &mut TestAppContext) {
974 init_test(cx);
975 let fs = project::FakeFs::new(cx.executor());
976 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
977 let context_server_registry =
978 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
979 let action_log = cx.new(|_| ActionLog::new(project.clone()));
980 let model = Arc::new(FakeLanguageModel::default());
981 let thread = cx.new(|cx| {
982 Thread::new(
983 project,
984 Rc::default(),
985 context_server_registry,
986 action_log.clone(),
987 Templates::new(),
988 Some(model.clone()),
989 cx,
990 )
991 });
992 let tool = Arc::new(EditFileTool { thread });
993 fs.insert_tree("/root", json!({})).await;
994
995 // Test 1: Path with .zed component should require confirmation
996 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
997 let _auth = cx.update(|cx| {
998 tool.authorize(
999 &EditFileToolInput {
1000 display_description: "test 1".into(),
1001 path: ".zed/settings.json".into(),
1002 mode: EditFileMode::Edit,
1003 },
1004 &stream_tx,
1005 cx,
1006 )
1007 });
1008
1009 let event = stream_rx.expect_authorization().await;
1010 assert_eq!(
1011 event.tool_call.fields.title,
1012 Some("test 1 (local settings)".into())
1013 );
1014
1015 // Test 2: Path outside project should require confirmation
1016 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1017 let _auth = cx.update(|cx| {
1018 tool.authorize(
1019 &EditFileToolInput {
1020 display_description: "test 2".into(),
1021 path: "/etc/hosts".into(),
1022 mode: EditFileMode::Edit,
1023 },
1024 &stream_tx,
1025 cx,
1026 )
1027 });
1028
1029 let event = stream_rx.expect_authorization().await;
1030 assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
1031
1032 // Test 3: Relative path without .zed should not require confirmation
1033 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1034 cx.update(|cx| {
1035 tool.authorize(
1036 &EditFileToolInput {
1037 display_description: "test 3".into(),
1038 path: "root/src/main.rs".into(),
1039 mode: EditFileMode::Edit,
1040 },
1041 &stream_tx,
1042 cx,
1043 )
1044 })
1045 .await
1046 .unwrap();
1047 assert!(stream_rx.try_next().is_err());
1048
1049 // Test 4: Path with .zed in the middle should require confirmation
1050 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1051 let _auth = cx.update(|cx| {
1052 tool.authorize(
1053 &EditFileToolInput {
1054 display_description: "test 4".into(),
1055 path: "root/.zed/tasks.json".into(),
1056 mode: EditFileMode::Edit,
1057 },
1058 &stream_tx,
1059 cx,
1060 )
1061 });
1062 let event = stream_rx.expect_authorization().await;
1063 assert_eq!(
1064 event.tool_call.fields.title,
1065 Some("test 4 (local settings)".into())
1066 );
1067
1068 // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
1069 cx.update(|cx| {
1070 let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1071 settings.always_allow_tool_actions = true;
1072 agent_settings::AgentSettings::override_global(settings, cx);
1073 });
1074
1075 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1076 cx.update(|cx| {
1077 tool.authorize(
1078 &EditFileToolInput {
1079 display_description: "test 5.1".into(),
1080 path: ".zed/settings.json".into(),
1081 mode: EditFileMode::Edit,
1082 },
1083 &stream_tx,
1084 cx,
1085 )
1086 })
1087 .await
1088 .unwrap();
1089 assert!(stream_rx.try_next().is_err());
1090
1091 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1092 cx.update(|cx| {
1093 tool.authorize(
1094 &EditFileToolInput {
1095 display_description: "test 5.2".into(),
1096 path: "/etc/hosts".into(),
1097 mode: EditFileMode::Edit,
1098 },
1099 &stream_tx,
1100 cx,
1101 )
1102 })
1103 .await
1104 .unwrap();
1105 assert!(stream_rx.try_next().is_err());
1106 }
1107
1108 #[gpui::test]
1109 async fn test_authorize_global_config(cx: &mut TestAppContext) {
1110 init_test(cx);
1111 let fs = project::FakeFs::new(cx.executor());
1112 fs.insert_tree("/project", json!({})).await;
1113 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1114 let context_server_registry =
1115 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1116 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1117 let model = Arc::new(FakeLanguageModel::default());
1118 let thread = cx.new(|cx| {
1119 Thread::new(
1120 project,
1121 Rc::default(),
1122 context_server_registry,
1123 action_log.clone(),
1124 Templates::new(),
1125 Some(model.clone()),
1126 cx,
1127 )
1128 });
1129 let tool = Arc::new(EditFileTool { thread });
1130
1131 // Test global config paths - these should require confirmation if they exist and are outside the project
1132 let test_cases = vec![
1133 (
1134 "/etc/hosts",
1135 true,
1136 "System file should require confirmation",
1137 ),
1138 (
1139 "/usr/local/bin/script",
1140 true,
1141 "System bin file should require confirmation",
1142 ),
1143 (
1144 "project/normal_file.rs",
1145 false,
1146 "Normal project file should not require confirmation",
1147 ),
1148 ];
1149
1150 for (path, should_confirm, description) in test_cases {
1151 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1152 let auth = cx.update(|cx| {
1153 tool.authorize(
1154 &EditFileToolInput {
1155 display_description: "Edit file".into(),
1156 path: path.into(),
1157 mode: EditFileMode::Edit,
1158 },
1159 &stream_tx,
1160 cx,
1161 )
1162 });
1163
1164 if should_confirm {
1165 stream_rx.expect_authorization().await;
1166 } else {
1167 auth.await.unwrap();
1168 assert!(
1169 stream_rx.try_next().is_err(),
1170 "Failed for case: {} - path: {} - expected no confirmation but got one",
1171 description,
1172 path
1173 );
1174 }
1175 }
1176 }
1177
1178 #[gpui::test]
1179 async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
1180 init_test(cx);
1181 let fs = project::FakeFs::new(cx.executor());
1182
1183 // Create multiple worktree directories
1184 fs.insert_tree(
1185 "/workspace/frontend",
1186 json!({
1187 "src": {
1188 "main.js": "console.log('frontend');"
1189 }
1190 }),
1191 )
1192 .await;
1193 fs.insert_tree(
1194 "/workspace/backend",
1195 json!({
1196 "src": {
1197 "main.rs": "fn main() {}"
1198 }
1199 }),
1200 )
1201 .await;
1202 fs.insert_tree(
1203 "/workspace/shared",
1204 json!({
1205 ".zed": {
1206 "settings.json": "{}"
1207 }
1208 }),
1209 )
1210 .await;
1211
1212 // Create project with multiple worktrees
1213 let project = Project::test(
1214 fs.clone(),
1215 [
1216 path!("/workspace/frontend").as_ref(),
1217 path!("/workspace/backend").as_ref(),
1218 path!("/workspace/shared").as_ref(),
1219 ],
1220 cx,
1221 )
1222 .await;
1223
1224 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1225 let context_server_registry =
1226 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1227 let model = Arc::new(FakeLanguageModel::default());
1228 let thread = cx.new(|cx| {
1229 Thread::new(
1230 project.clone(),
1231 Rc::default(),
1232 context_server_registry.clone(),
1233 action_log.clone(),
1234 Templates::new(),
1235 Some(model.clone()),
1236 cx,
1237 )
1238 });
1239 let tool = Arc::new(EditFileTool { thread });
1240
1241 // Test files in different worktrees
1242 let test_cases = vec![
1243 ("frontend/src/main.js", false, "File in first worktree"),
1244 ("backend/src/main.rs", false, "File in second worktree"),
1245 (
1246 "shared/.zed/settings.json",
1247 true,
1248 ".zed file in third worktree",
1249 ),
1250 ("/etc/hosts", true, "Absolute path outside all worktrees"),
1251 (
1252 "../outside/file.txt",
1253 true,
1254 "Relative path outside worktrees",
1255 ),
1256 ];
1257
1258 for (path, should_confirm, description) in test_cases {
1259 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1260 let auth = cx.update(|cx| {
1261 tool.authorize(
1262 &EditFileToolInput {
1263 display_description: "Edit file".into(),
1264 path: path.into(),
1265 mode: EditFileMode::Edit,
1266 },
1267 &stream_tx,
1268 cx,
1269 )
1270 });
1271
1272 if should_confirm {
1273 stream_rx.expect_authorization().await;
1274 } else {
1275 auth.await.unwrap();
1276 assert!(
1277 stream_rx.try_next().is_err(),
1278 "Failed for case: {} - path: {} - expected no confirmation but got one",
1279 description,
1280 path
1281 );
1282 }
1283 }
1284 }
1285
1286 #[gpui::test]
1287 async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
1288 init_test(cx);
1289 let fs = project::FakeFs::new(cx.executor());
1290 fs.insert_tree(
1291 "/project",
1292 json!({
1293 ".zed": {
1294 "settings.json": "{}"
1295 },
1296 "src": {
1297 ".zed": {
1298 "local.json": "{}"
1299 }
1300 }
1301 }),
1302 )
1303 .await;
1304 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1305 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1306 let context_server_registry =
1307 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1308 let model = Arc::new(FakeLanguageModel::default());
1309 let thread = cx.new(|cx| {
1310 Thread::new(
1311 project.clone(),
1312 Rc::default(),
1313 context_server_registry.clone(),
1314 action_log.clone(),
1315 Templates::new(),
1316 Some(model.clone()),
1317 cx,
1318 )
1319 });
1320 let tool = Arc::new(EditFileTool { thread });
1321
1322 // Test edge cases
1323 let test_cases = vec![
1324 // Empty path - find_project_path returns Some for empty paths
1325 ("", false, "Empty path is treated as project root"),
1326 // Root directory
1327 ("/", true, "Root directory should be outside project"),
1328 // Parent directory references - find_project_path resolves these
1329 (
1330 "project/../other",
1331 false,
1332 "Path with .. is resolved by find_project_path",
1333 ),
1334 (
1335 "project/./src/file.rs",
1336 false,
1337 "Path with . should work normally",
1338 ),
1339 // Windows-style paths (if on Windows)
1340 #[cfg(target_os = "windows")]
1341 ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
1342 #[cfg(target_os = "windows")]
1343 ("project\\src\\main.rs", false, "Windows-style project path"),
1344 ];
1345
1346 for (path, should_confirm, description) in test_cases {
1347 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1348 let auth = cx.update(|cx| {
1349 tool.authorize(
1350 &EditFileToolInput {
1351 display_description: "Edit file".into(),
1352 path: path.into(),
1353 mode: EditFileMode::Edit,
1354 },
1355 &stream_tx,
1356 cx,
1357 )
1358 });
1359
1360 if should_confirm {
1361 stream_rx.expect_authorization().await;
1362 } else {
1363 auth.await.unwrap();
1364 assert!(
1365 stream_rx.try_next().is_err(),
1366 "Failed for case: {} - path: {} - expected no confirmation but got one",
1367 description,
1368 path
1369 );
1370 }
1371 }
1372 }
1373
1374 #[gpui::test]
1375 async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
1376 init_test(cx);
1377 let fs = project::FakeFs::new(cx.executor());
1378 fs.insert_tree(
1379 "/project",
1380 json!({
1381 "existing.txt": "content",
1382 ".zed": {
1383 "settings.json": "{}"
1384 }
1385 }),
1386 )
1387 .await;
1388 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1389 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1390 let context_server_registry =
1391 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1392 let model = Arc::new(FakeLanguageModel::default());
1393 let thread = cx.new(|cx| {
1394 Thread::new(
1395 project.clone(),
1396 Rc::default(),
1397 context_server_registry.clone(),
1398 action_log.clone(),
1399 Templates::new(),
1400 Some(model.clone()),
1401 cx,
1402 )
1403 });
1404 let tool = Arc::new(EditFileTool { thread });
1405
1406 // Test different EditFileMode values
1407 let modes = vec![
1408 EditFileMode::Edit,
1409 EditFileMode::Create,
1410 EditFileMode::Overwrite,
1411 ];
1412
1413 for mode in modes {
1414 // Test .zed path with different modes
1415 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1416 let _auth = cx.update(|cx| {
1417 tool.authorize(
1418 &EditFileToolInput {
1419 display_description: "Edit settings".into(),
1420 path: "project/.zed/settings.json".into(),
1421 mode: mode.clone(),
1422 },
1423 &stream_tx,
1424 cx,
1425 )
1426 });
1427
1428 stream_rx.expect_authorization().await;
1429
1430 // Test outside path with different modes
1431 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1432 let _auth = cx.update(|cx| {
1433 tool.authorize(
1434 &EditFileToolInput {
1435 display_description: "Edit file".into(),
1436 path: "/outside/file.txt".into(),
1437 mode: mode.clone(),
1438 },
1439 &stream_tx,
1440 cx,
1441 )
1442 });
1443
1444 stream_rx.expect_authorization().await;
1445
1446 // Test normal path with different modes
1447 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1448 cx.update(|cx| {
1449 tool.authorize(
1450 &EditFileToolInput {
1451 display_description: "Edit file".into(),
1452 path: "project/normal.txt".into(),
1453 mode: mode.clone(),
1454 },
1455 &stream_tx,
1456 cx,
1457 )
1458 })
1459 .await
1460 .unwrap();
1461 assert!(stream_rx.try_next().is_err());
1462 }
1463 }
1464
1465 #[gpui::test]
1466 async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
1467 init_test(cx);
1468 let fs = project::FakeFs::new(cx.executor());
1469 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1470 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1471 let context_server_registry =
1472 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1473 let model = Arc::new(FakeLanguageModel::default());
1474 let thread = cx.new(|cx| {
1475 Thread::new(
1476 project.clone(),
1477 Rc::default(),
1478 context_server_registry,
1479 action_log.clone(),
1480 Templates::new(),
1481 Some(model.clone()),
1482 cx,
1483 )
1484 });
1485 let tool = Arc::new(EditFileTool { thread });
1486
1487 assert_eq!(
1488 tool.initial_title(Err(json!({
1489 "path": "src/main.rs",
1490 "display_description": "",
1491 "old_string": "old code",
1492 "new_string": "new code"
1493 }))),
1494 "src/main.rs"
1495 );
1496 assert_eq!(
1497 tool.initial_title(Err(json!({
1498 "path": "",
1499 "display_description": "Fix error handling",
1500 "old_string": "old code",
1501 "new_string": "new code"
1502 }))),
1503 "Fix error handling"
1504 );
1505 assert_eq!(
1506 tool.initial_title(Err(json!({
1507 "path": "src/main.rs",
1508 "display_description": "Fix error handling",
1509 "old_string": "old code",
1510 "new_string": "new code"
1511 }))),
1512 "Fix error handling"
1513 );
1514 assert_eq!(
1515 tool.initial_title(Err(json!({
1516 "path": "",
1517 "display_description": "",
1518 "old_string": "old code",
1519 "new_string": "new code"
1520 }))),
1521 DEFAULT_UI_TEXT
1522 );
1523 assert_eq!(
1524 tool.initial_title(Err(serde_json::Value::Null)),
1525 DEFAULT_UI_TEXT
1526 );
1527 }
1528
1529 fn init_test(cx: &mut TestAppContext) {
1530 cx.update(|cx| {
1531 let settings_store = SettingsStore::test(cx);
1532 cx.set_global(settings_store);
1533 language::init(cx);
1534 TelemetrySettings::register(cx);
1535 agent_settings::AgentSettings::register(cx);
1536 Project::init_settings(cx);
1537 });
1538 }
1539}