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, WeakEntity};
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: WeakEntity<Thread>,
126}
127
128impl EditFileTool {
129 pub fn new(thread: WeakEntity<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 Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
171 thread.project().read(cx).find_project_path(&input.path, cx)
172 }) else {
173 return Task::ready(Err(anyhow!("thread was dropped")));
174 };
175
176 // If the path is inside the project, and it's not one of the above edge cases,
177 // then no confirmation is necessary. Otherwise, confirmation is necessary.
178 if project_path.is_some() {
179 Task::ready(Ok(()))
180 } else {
181 event_stream.authorize(&input.display_description, cx)
182 }
183 }
184}
185
186impl AgentTool for EditFileTool {
187 type Input = EditFileToolInput;
188 type Output = EditFileToolOutput;
189
190 fn name(&self) -> SharedString {
191 "edit_file".into()
192 }
193
194 fn kind(&self) -> acp::ToolKind {
195 acp::ToolKind::Edit
196 }
197
198 fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
199 match input {
200 Ok(input) => input.display_description.into(),
201 Err(raw_input) => {
202 if let Some(input) =
203 serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
204 {
205 let description = input.display_description.trim();
206 if !description.is_empty() {
207 return description.to_string().into();
208 }
209
210 let path = input.path.trim().to_string();
211 if !path.is_empty() {
212 return path.into();
213 }
214 }
215
216 DEFAULT_UI_TEXT.into()
217 }
218 }
219 }
220
221 fn run(
222 self: Arc<Self>,
223 input: Self::Input,
224 event_stream: ToolCallEventStream,
225 cx: &mut App,
226 ) -> Task<Result<Self::Output>> {
227 let Ok(project) = self
228 .thread
229 .read_with(cx, |thread, _cx| thread.project().clone())
230 else {
231 return Task::ready(Err(anyhow!("thread was dropped")));
232 };
233 let project_path = match resolve_path(&input, project.clone(), cx) {
234 Ok(path) => path,
235 Err(err) => return Task::ready(Err(anyhow!(err))),
236 };
237 let abs_path = project.read(cx).absolute_path(&project_path, cx);
238 if let Some(abs_path) = abs_path.clone() {
239 event_stream.update_fields(ToolCallUpdateFields {
240 locations: Some(vec![acp::ToolCallLocation {
241 path: abs_path,
242 line: None,
243 }]),
244 ..Default::default()
245 });
246 }
247
248 let authorize = self.authorize(&input, &event_stream, cx);
249 cx.spawn(async move |cx: &mut AsyncApp| {
250 authorize.await?;
251
252 let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
253 let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
254 (request, thread.model().clone(), thread.action_log().clone())
255 })?;
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 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::new(thread.downgrade())).run(
541 input,
542 ToolCallEventStream::test().0,
543 cx,
544 )
545 })
546 .await;
547 assert_eq!(
548 result.unwrap_err().to_string(),
549 "Can't edit file: path not found"
550 );
551 }
552
553 #[gpui::test]
554 async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
555 let mode = &EditFileMode::Create;
556
557 let result = test_resolve_path(mode, "root/new.txt", cx);
558 assert_resolved_path_eq(result.await, "new.txt");
559
560 let result = test_resolve_path(mode, "new.txt", cx);
561 assert_resolved_path_eq(result.await, "new.txt");
562
563 let result = test_resolve_path(mode, "dir/new.txt", cx);
564 assert_resolved_path_eq(result.await, "dir/new.txt");
565
566 let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
567 assert_eq!(
568 result.await.unwrap_err().to_string(),
569 "Can't create file: file already exists"
570 );
571
572 let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
573 assert_eq!(
574 result.await.unwrap_err().to_string(),
575 "Can't create file: parent directory doesn't exist"
576 );
577 }
578
579 #[gpui::test]
580 async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
581 let mode = &EditFileMode::Edit;
582
583 let path_with_root = "root/dir/subdir/existing.txt";
584 let path_without_root = "dir/subdir/existing.txt";
585 let result = test_resolve_path(mode, path_with_root, cx);
586 assert_resolved_path_eq(result.await, path_without_root);
587
588 let result = test_resolve_path(mode, path_without_root, cx);
589 assert_resolved_path_eq(result.await, path_without_root);
590
591 let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
592 assert_eq!(
593 result.await.unwrap_err().to_string(),
594 "Can't edit file: path not found"
595 );
596
597 let result = test_resolve_path(mode, "root/dir", cx);
598 assert_eq!(
599 result.await.unwrap_err().to_string(),
600 "Can't edit file: path is a directory"
601 );
602 }
603
604 async fn test_resolve_path(
605 mode: &EditFileMode,
606 path: &str,
607 cx: &mut TestAppContext,
608 ) -> anyhow::Result<ProjectPath> {
609 init_test(cx);
610
611 let fs = project::FakeFs::new(cx.executor());
612 fs.insert_tree(
613 "/root",
614 json!({
615 "dir": {
616 "subdir": {
617 "existing.txt": "hello"
618 }
619 }
620 }),
621 )
622 .await;
623 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
624
625 let input = EditFileToolInput {
626 display_description: "Some edit".into(),
627 path: path.into(),
628 mode: mode.clone(),
629 };
630
631 let result = cx.update(|cx| resolve_path(&input, project, cx));
632 result
633 }
634
635 fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
636 let actual = path
637 .expect("Should return valid path")
638 .path
639 .to_str()
640 .unwrap()
641 .replace("\\", "/"); // Naive Windows paths normalization
642 assert_eq!(actual, expected);
643 }
644
645 #[gpui::test]
646 async fn test_format_on_save(cx: &mut TestAppContext) {
647 init_test(cx);
648
649 let fs = project::FakeFs::new(cx.executor());
650 fs.insert_tree("/root", json!({"src": {}})).await;
651
652 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
653
654 // Set up a Rust language with LSP formatting support
655 let rust_language = Arc::new(language::Language::new(
656 language::LanguageConfig {
657 name: "Rust".into(),
658 matcher: language::LanguageMatcher {
659 path_suffixes: vec!["rs".to_string()],
660 ..Default::default()
661 },
662 ..Default::default()
663 },
664 None,
665 ));
666
667 // Register the language and fake LSP
668 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
669 language_registry.add(rust_language);
670
671 let mut fake_language_servers = language_registry.register_fake_lsp(
672 "Rust",
673 language::FakeLspAdapter {
674 capabilities: lsp::ServerCapabilities {
675 document_formatting_provider: Some(lsp::OneOf::Left(true)),
676 ..Default::default()
677 },
678 ..Default::default()
679 },
680 );
681
682 // Create the file
683 fs.save(
684 path!("/root/src/main.rs").as_ref(),
685 &"initial content".into(),
686 language::LineEnding::Unix,
687 )
688 .await
689 .unwrap();
690
691 // Open the buffer to trigger LSP initialization
692 let buffer = project
693 .update(cx, |project, cx| {
694 project.open_local_buffer(path!("/root/src/main.rs"), cx)
695 })
696 .await
697 .unwrap();
698
699 // Register the buffer with language servers
700 let _handle = project.update(cx, |project, cx| {
701 project.register_buffer_with_language_servers(&buffer, cx)
702 });
703
704 const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
705 const FORMATTED_CONTENT: &str =
706 "This file was formatted by the fake formatter in the test.\n";
707
708 // Get the fake language server and set up formatting handler
709 let fake_language_server = fake_language_servers.next().await.unwrap();
710 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
711 |_, _| async move {
712 Ok(Some(vec![lsp::TextEdit {
713 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
714 new_text: FORMATTED_CONTENT.to_string(),
715 }]))
716 }
717 });
718
719 let action_log = cx.new(|_| ActionLog::new(project.clone()));
720 let context_server_registry =
721 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
722 let model = Arc::new(FakeLanguageModel::default());
723 let thread = cx.new(|cx| {
724 Thread::new(
725 project,
726 Rc::default(),
727 context_server_registry,
728 action_log.clone(),
729 Templates::new(),
730 model.clone(),
731 cx,
732 )
733 });
734
735 // First, test with format_on_save enabled
736 cx.update(|cx| {
737 SettingsStore::update_global(cx, |store, cx| {
738 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
739 cx,
740 |settings| {
741 settings.defaults.format_on_save = Some(FormatOnSave::On);
742 settings.defaults.formatter =
743 Some(language::language_settings::SelectedFormatter::Auto);
744 },
745 );
746 });
747 });
748
749 // Have the model stream unformatted content
750 let edit_result = {
751 let edit_task = cx.update(|cx| {
752 let input = EditFileToolInput {
753 display_description: "Create main function".into(),
754 path: "root/src/main.rs".into(),
755 mode: EditFileMode::Overwrite,
756 };
757 Arc::new(EditFileTool::new(thread.downgrade())).run(
758 input,
759 ToolCallEventStream::test().0,
760 cx,
761 )
762 });
763
764 // Stream the unformatted content
765 cx.executor().run_until_parked();
766 model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
767 model.end_last_completion_stream();
768
769 edit_task.await
770 };
771 assert!(edit_result.is_ok());
772
773 // Wait for any async operations (e.g. formatting) to complete
774 cx.executor().run_until_parked();
775
776 // Read the file to verify it was formatted automatically
777 let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
778 assert_eq!(
779 // Ignore carriage returns on Windows
780 new_content.replace("\r\n", "\n"),
781 FORMATTED_CONTENT,
782 "Code should be formatted when format_on_save is enabled"
783 );
784
785 let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
786
787 assert_eq!(
788 stale_buffer_count, 0,
789 "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \
790 This causes the agent to think the file was modified externally when it was just formatted.",
791 stale_buffer_count
792 );
793
794 // Next, test with format_on_save disabled
795 cx.update(|cx| {
796 SettingsStore::update_global(cx, |store, cx| {
797 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
798 cx,
799 |settings| {
800 settings.defaults.format_on_save = Some(FormatOnSave::Off);
801 },
802 );
803 });
804 });
805
806 // Stream unformatted edits again
807 let edit_result = {
808 let edit_task = cx.update(|cx| {
809 let input = EditFileToolInput {
810 display_description: "Update main function".into(),
811 path: "root/src/main.rs".into(),
812 mode: EditFileMode::Overwrite,
813 };
814 Arc::new(EditFileTool::new(thread.downgrade())).run(
815 input,
816 ToolCallEventStream::test().0,
817 cx,
818 )
819 });
820
821 // Stream the unformatted content
822 cx.executor().run_until_parked();
823 model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string());
824 model.end_last_completion_stream();
825
826 edit_task.await
827 };
828 assert!(edit_result.is_ok());
829
830 // Wait for any async operations (e.g. formatting) to complete
831 cx.executor().run_until_parked();
832
833 // Verify the file was not formatted
834 let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
835 assert_eq!(
836 // Ignore carriage returns on Windows
837 new_content.replace("\r\n", "\n"),
838 UNFORMATTED_CONTENT,
839 "Code should not be formatted when format_on_save is disabled"
840 );
841 }
842
843 #[gpui::test]
844 async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
845 init_test(cx);
846
847 let fs = project::FakeFs::new(cx.executor());
848 fs.insert_tree("/root", json!({"src": {}})).await;
849
850 // Create a simple file with trailing whitespace
851 fs.save(
852 path!("/root/src/main.rs").as_ref(),
853 &"initial content".into(),
854 language::LineEnding::Unix,
855 )
856 .await
857 .unwrap();
858
859 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
860 let context_server_registry =
861 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
862 let action_log = cx.new(|_| ActionLog::new(project.clone()));
863 let model = Arc::new(FakeLanguageModel::default());
864 let thread = cx.new(|cx| {
865 Thread::new(
866 project,
867 Rc::default(),
868 context_server_registry,
869 action_log.clone(),
870 Templates::new(),
871 model.clone(),
872 cx,
873 )
874 });
875
876 // First, test with remove_trailing_whitespace_on_save enabled
877 cx.update(|cx| {
878 SettingsStore::update_global(cx, |store, cx| {
879 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
880 cx,
881 |settings| {
882 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
883 },
884 );
885 });
886 });
887
888 const CONTENT_WITH_TRAILING_WHITESPACE: &str =
889 "fn main() { \n println!(\"Hello!\"); \n}\n";
890
891 // Have the model stream content that contains trailing whitespace
892 let edit_result = {
893 let edit_task = cx.update(|cx| {
894 let input = EditFileToolInput {
895 display_description: "Create main function".into(),
896 path: "root/src/main.rs".into(),
897 mode: EditFileMode::Overwrite,
898 };
899 Arc::new(EditFileTool::new(thread.downgrade())).run(
900 input,
901 ToolCallEventStream::test().0,
902 cx,
903 )
904 });
905
906 // Stream the content with trailing whitespace
907 cx.executor().run_until_parked();
908 model.send_last_completion_stream_text_chunk(
909 CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
910 );
911 model.end_last_completion_stream();
912
913 edit_task.await
914 };
915 assert!(edit_result.is_ok());
916
917 // Wait for any async operations (e.g. formatting) to complete
918 cx.executor().run_until_parked();
919
920 // Read the file to verify trailing whitespace was removed automatically
921 assert_eq!(
922 // Ignore carriage returns on Windows
923 fs.load(path!("/root/src/main.rs").as_ref())
924 .await
925 .unwrap()
926 .replace("\r\n", "\n"),
927 "fn main() {\n println!(\"Hello!\");\n}\n",
928 "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
929 );
930
931 // Next, test with remove_trailing_whitespace_on_save disabled
932 cx.update(|cx| {
933 SettingsStore::update_global(cx, |store, cx| {
934 store.update_user_settings::<language::language_settings::AllLanguageSettings>(
935 cx,
936 |settings| {
937 settings.defaults.remove_trailing_whitespace_on_save = Some(false);
938 },
939 );
940 });
941 });
942
943 // Stream edits again with trailing whitespace
944 let edit_result = {
945 let edit_task = cx.update(|cx| {
946 let input = EditFileToolInput {
947 display_description: "Update main function".into(),
948 path: "root/src/main.rs".into(),
949 mode: EditFileMode::Overwrite,
950 };
951 Arc::new(EditFileTool::new(thread.downgrade())).run(
952 input,
953 ToolCallEventStream::test().0,
954 cx,
955 )
956 });
957
958 // Stream the content with trailing whitespace
959 cx.executor().run_until_parked();
960 model.send_last_completion_stream_text_chunk(
961 CONTENT_WITH_TRAILING_WHITESPACE.to_string(),
962 );
963 model.end_last_completion_stream();
964
965 edit_task.await
966 };
967 assert!(edit_result.is_ok());
968
969 // Wait for any async operations (e.g. formatting) to complete
970 cx.executor().run_until_parked();
971
972 // Verify the file still has trailing whitespace
973 // Read the file again - it should still have trailing whitespace
974 let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
975 assert_eq!(
976 // Ignore carriage returns on Windows
977 final_content.replace("\r\n", "\n"),
978 CONTENT_WITH_TRAILING_WHITESPACE,
979 "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
980 );
981 }
982
983 #[gpui::test]
984 async fn test_authorize(cx: &mut TestAppContext) {
985 init_test(cx);
986 let fs = project::FakeFs::new(cx.executor());
987 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
988 let context_server_registry =
989 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
990 let action_log = cx.new(|_| ActionLog::new(project.clone()));
991 let model = Arc::new(FakeLanguageModel::default());
992 let thread = cx.new(|cx| {
993 Thread::new(
994 project,
995 Rc::default(),
996 context_server_registry,
997 action_log.clone(),
998 Templates::new(),
999 model.clone(),
1000 cx,
1001 )
1002 });
1003 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1004 fs.insert_tree("/root", json!({})).await;
1005
1006 // Test 1: Path with .zed component should require confirmation
1007 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1008 let _auth = cx.update(|cx| {
1009 tool.authorize(
1010 &EditFileToolInput {
1011 display_description: "test 1".into(),
1012 path: ".zed/settings.json".into(),
1013 mode: EditFileMode::Edit,
1014 },
1015 &stream_tx,
1016 cx,
1017 )
1018 });
1019
1020 let event = stream_rx.expect_authorization().await;
1021 assert_eq!(event.tool_call.title, "test 1 (local settings)");
1022
1023 // Test 2: Path outside project should require confirmation
1024 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1025 let _auth = cx.update(|cx| {
1026 tool.authorize(
1027 &EditFileToolInput {
1028 display_description: "test 2".into(),
1029 path: "/etc/hosts".into(),
1030 mode: EditFileMode::Edit,
1031 },
1032 &stream_tx,
1033 cx,
1034 )
1035 });
1036
1037 let event = stream_rx.expect_authorization().await;
1038 assert_eq!(event.tool_call.title, "test 2");
1039
1040 // Test 3: Relative path without .zed should not require confirmation
1041 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1042 cx.update(|cx| {
1043 tool.authorize(
1044 &EditFileToolInput {
1045 display_description: "test 3".into(),
1046 path: "root/src/main.rs".into(),
1047 mode: EditFileMode::Edit,
1048 },
1049 &stream_tx,
1050 cx,
1051 )
1052 })
1053 .await
1054 .unwrap();
1055 assert!(stream_rx.try_next().is_err());
1056
1057 // Test 4: Path with .zed in the middle should require confirmation
1058 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1059 let _auth = cx.update(|cx| {
1060 tool.authorize(
1061 &EditFileToolInput {
1062 display_description: "test 4".into(),
1063 path: "root/.zed/tasks.json".into(),
1064 mode: EditFileMode::Edit,
1065 },
1066 &stream_tx,
1067 cx,
1068 )
1069 });
1070 let event = stream_rx.expect_authorization().await;
1071 assert_eq!(event.tool_call.title, "test 4 (local settings)");
1072
1073 // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
1074 cx.update(|cx| {
1075 let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
1076 settings.always_allow_tool_actions = true;
1077 agent_settings::AgentSettings::override_global(settings, cx);
1078 });
1079
1080 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1081 cx.update(|cx| {
1082 tool.authorize(
1083 &EditFileToolInput {
1084 display_description: "test 5.1".into(),
1085 path: ".zed/settings.json".into(),
1086 mode: EditFileMode::Edit,
1087 },
1088 &stream_tx,
1089 cx,
1090 )
1091 })
1092 .await
1093 .unwrap();
1094 assert!(stream_rx.try_next().is_err());
1095
1096 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1097 cx.update(|cx| {
1098 tool.authorize(
1099 &EditFileToolInput {
1100 display_description: "test 5.2".into(),
1101 path: "/etc/hosts".into(),
1102 mode: EditFileMode::Edit,
1103 },
1104 &stream_tx,
1105 cx,
1106 )
1107 })
1108 .await
1109 .unwrap();
1110 assert!(stream_rx.try_next().is_err());
1111 }
1112
1113 #[gpui::test]
1114 async fn test_authorize_global_config(cx: &mut TestAppContext) {
1115 init_test(cx);
1116 let fs = project::FakeFs::new(cx.executor());
1117 fs.insert_tree("/project", json!({})).await;
1118 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1119 let context_server_registry =
1120 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1121 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1122 let model = Arc::new(FakeLanguageModel::default());
1123 let thread = cx.new(|cx| {
1124 Thread::new(
1125 project,
1126 Rc::default(),
1127 context_server_registry,
1128 action_log.clone(),
1129 Templates::new(),
1130 model.clone(),
1131 cx,
1132 )
1133 });
1134 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1135
1136 // Test global config paths - these should require confirmation if they exist and are outside the project
1137 let test_cases = vec![
1138 (
1139 "/etc/hosts",
1140 true,
1141 "System file should require confirmation",
1142 ),
1143 (
1144 "/usr/local/bin/script",
1145 true,
1146 "System bin file should require confirmation",
1147 ),
1148 (
1149 "project/normal_file.rs",
1150 false,
1151 "Normal project file should not require confirmation",
1152 ),
1153 ];
1154
1155 for (path, should_confirm, description) in test_cases {
1156 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1157 let auth = cx.update(|cx| {
1158 tool.authorize(
1159 &EditFileToolInput {
1160 display_description: "Edit file".into(),
1161 path: path.into(),
1162 mode: EditFileMode::Edit,
1163 },
1164 &stream_tx,
1165 cx,
1166 )
1167 });
1168
1169 if should_confirm {
1170 stream_rx.expect_authorization().await;
1171 } else {
1172 auth.await.unwrap();
1173 assert!(
1174 stream_rx.try_next().is_err(),
1175 "Failed for case: {} - path: {} - expected no confirmation but got one",
1176 description,
1177 path
1178 );
1179 }
1180 }
1181 }
1182
1183 #[gpui::test]
1184 async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
1185 init_test(cx);
1186 let fs = project::FakeFs::new(cx.executor());
1187
1188 // Create multiple worktree directories
1189 fs.insert_tree(
1190 "/workspace/frontend",
1191 json!({
1192 "src": {
1193 "main.js": "console.log('frontend');"
1194 }
1195 }),
1196 )
1197 .await;
1198 fs.insert_tree(
1199 "/workspace/backend",
1200 json!({
1201 "src": {
1202 "main.rs": "fn main() {}"
1203 }
1204 }),
1205 )
1206 .await;
1207 fs.insert_tree(
1208 "/workspace/shared",
1209 json!({
1210 ".zed": {
1211 "settings.json": "{}"
1212 }
1213 }),
1214 )
1215 .await;
1216
1217 // Create project with multiple worktrees
1218 let project = Project::test(
1219 fs.clone(),
1220 [
1221 path!("/workspace/frontend").as_ref(),
1222 path!("/workspace/backend").as_ref(),
1223 path!("/workspace/shared").as_ref(),
1224 ],
1225 cx,
1226 )
1227 .await;
1228
1229 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1230 let context_server_registry =
1231 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1232 let model = Arc::new(FakeLanguageModel::default());
1233 let thread = cx.new(|cx| {
1234 Thread::new(
1235 project.clone(),
1236 Rc::default(),
1237 context_server_registry.clone(),
1238 action_log.clone(),
1239 Templates::new(),
1240 model.clone(),
1241 cx,
1242 )
1243 });
1244 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1245
1246 // Test files in different worktrees
1247 let test_cases = vec![
1248 ("frontend/src/main.js", false, "File in first worktree"),
1249 ("backend/src/main.rs", false, "File in second worktree"),
1250 (
1251 "shared/.zed/settings.json",
1252 true,
1253 ".zed file in third worktree",
1254 ),
1255 ("/etc/hosts", true, "Absolute path outside all worktrees"),
1256 (
1257 "../outside/file.txt",
1258 true,
1259 "Relative path outside worktrees",
1260 ),
1261 ];
1262
1263 for (path, should_confirm, description) in test_cases {
1264 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1265 let auth = cx.update(|cx| {
1266 tool.authorize(
1267 &EditFileToolInput {
1268 display_description: "Edit file".into(),
1269 path: path.into(),
1270 mode: EditFileMode::Edit,
1271 },
1272 &stream_tx,
1273 cx,
1274 )
1275 });
1276
1277 if should_confirm {
1278 stream_rx.expect_authorization().await;
1279 } else {
1280 auth.await.unwrap();
1281 assert!(
1282 stream_rx.try_next().is_err(),
1283 "Failed for case: {} - path: {} - expected no confirmation but got one",
1284 description,
1285 path
1286 );
1287 }
1288 }
1289 }
1290
1291 #[gpui::test]
1292 async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
1293 init_test(cx);
1294 let fs = project::FakeFs::new(cx.executor());
1295 fs.insert_tree(
1296 "/project",
1297 json!({
1298 ".zed": {
1299 "settings.json": "{}"
1300 },
1301 "src": {
1302 ".zed": {
1303 "local.json": "{}"
1304 }
1305 }
1306 }),
1307 )
1308 .await;
1309 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1310 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1311 let context_server_registry =
1312 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1313 let model = Arc::new(FakeLanguageModel::default());
1314 let thread = cx.new(|cx| {
1315 Thread::new(
1316 project.clone(),
1317 Rc::default(),
1318 context_server_registry.clone(),
1319 action_log.clone(),
1320 Templates::new(),
1321 model.clone(),
1322 cx,
1323 )
1324 });
1325 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1326
1327 // Test edge cases
1328 let test_cases = vec![
1329 // Empty path - find_project_path returns Some for empty paths
1330 ("", false, "Empty path is treated as project root"),
1331 // Root directory
1332 ("/", true, "Root directory should be outside project"),
1333 // Parent directory references - find_project_path resolves these
1334 (
1335 "project/../other",
1336 false,
1337 "Path with .. is resolved by find_project_path",
1338 ),
1339 (
1340 "project/./src/file.rs",
1341 false,
1342 "Path with . should work normally",
1343 ),
1344 // Windows-style paths (if on Windows)
1345 #[cfg(target_os = "windows")]
1346 ("C:\\Windows\\System32\\hosts", true, "Windows system path"),
1347 #[cfg(target_os = "windows")]
1348 ("project\\src\\main.rs", false, "Windows-style project path"),
1349 ];
1350
1351 for (path, should_confirm, description) in test_cases {
1352 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1353 let auth = cx.update(|cx| {
1354 tool.authorize(
1355 &EditFileToolInput {
1356 display_description: "Edit file".into(),
1357 path: path.into(),
1358 mode: EditFileMode::Edit,
1359 },
1360 &stream_tx,
1361 cx,
1362 )
1363 });
1364
1365 if should_confirm {
1366 stream_rx.expect_authorization().await;
1367 } else {
1368 auth.await.unwrap();
1369 assert!(
1370 stream_rx.try_next().is_err(),
1371 "Failed for case: {} - path: {} - expected no confirmation but got one",
1372 description,
1373 path
1374 );
1375 }
1376 }
1377 }
1378
1379 #[gpui::test]
1380 async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
1381 init_test(cx);
1382 let fs = project::FakeFs::new(cx.executor());
1383 fs.insert_tree(
1384 "/project",
1385 json!({
1386 "existing.txt": "content",
1387 ".zed": {
1388 "settings.json": "{}"
1389 }
1390 }),
1391 )
1392 .await;
1393 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1394 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1395 let context_server_registry =
1396 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1397 let model = Arc::new(FakeLanguageModel::default());
1398 let thread = cx.new(|cx| {
1399 Thread::new(
1400 project.clone(),
1401 Rc::default(),
1402 context_server_registry.clone(),
1403 action_log.clone(),
1404 Templates::new(),
1405 model.clone(),
1406 cx,
1407 )
1408 });
1409 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1410
1411 // Test different EditFileMode values
1412 let modes = vec![
1413 EditFileMode::Edit,
1414 EditFileMode::Create,
1415 EditFileMode::Overwrite,
1416 ];
1417
1418 for mode in modes {
1419 // Test .zed path with different modes
1420 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1421 let _auth = cx.update(|cx| {
1422 tool.authorize(
1423 &EditFileToolInput {
1424 display_description: "Edit settings".into(),
1425 path: "project/.zed/settings.json".into(),
1426 mode: mode.clone(),
1427 },
1428 &stream_tx,
1429 cx,
1430 )
1431 });
1432
1433 stream_rx.expect_authorization().await;
1434
1435 // Test outside path with different modes
1436 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1437 let _auth = cx.update(|cx| {
1438 tool.authorize(
1439 &EditFileToolInput {
1440 display_description: "Edit file".into(),
1441 path: "/outside/file.txt".into(),
1442 mode: mode.clone(),
1443 },
1444 &stream_tx,
1445 cx,
1446 )
1447 });
1448
1449 stream_rx.expect_authorization().await;
1450
1451 // Test normal path with different modes
1452 let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
1453 cx.update(|cx| {
1454 tool.authorize(
1455 &EditFileToolInput {
1456 display_description: "Edit file".into(),
1457 path: "project/normal.txt".into(),
1458 mode: mode.clone(),
1459 },
1460 &stream_tx,
1461 cx,
1462 )
1463 })
1464 .await
1465 .unwrap();
1466 assert!(stream_rx.try_next().is_err());
1467 }
1468 }
1469
1470 #[gpui::test]
1471 async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) {
1472 init_test(cx);
1473 let fs = project::FakeFs::new(cx.executor());
1474 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1475 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1476 let context_server_registry =
1477 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1478 let model = Arc::new(FakeLanguageModel::default());
1479 let thread = cx.new(|cx| {
1480 Thread::new(
1481 project.clone(),
1482 Rc::default(),
1483 context_server_registry,
1484 action_log.clone(),
1485 Templates::new(),
1486 model.clone(),
1487 cx,
1488 )
1489 });
1490 let tool = Arc::new(EditFileTool::new(thread.downgrade()));
1491
1492 assert_eq!(
1493 tool.initial_title(Err(json!({
1494 "path": "src/main.rs",
1495 "display_description": "",
1496 "old_string": "old code",
1497 "new_string": "new code"
1498 }))),
1499 "src/main.rs"
1500 );
1501 assert_eq!(
1502 tool.initial_title(Err(json!({
1503 "path": "",
1504 "display_description": "Fix error handling",
1505 "old_string": "old code",
1506 "new_string": "new code"
1507 }))),
1508 "Fix error handling"
1509 );
1510 assert_eq!(
1511 tool.initial_title(Err(json!({
1512 "path": "src/main.rs",
1513 "display_description": "Fix error handling",
1514 "old_string": "old code",
1515 "new_string": "new code"
1516 }))),
1517 "Fix error handling"
1518 );
1519 assert_eq!(
1520 tool.initial_title(Err(json!({
1521 "path": "",
1522 "display_description": "",
1523 "old_string": "old code",
1524 "new_string": "new code"
1525 }))),
1526 DEFAULT_UI_TEXT
1527 );
1528 assert_eq!(
1529 tool.initial_title(Err(serde_json::Value::Null)),
1530 DEFAULT_UI_TEXT
1531 );
1532 }
1533
1534 fn init_test(cx: &mut TestAppContext) {
1535 cx.update(|cx| {
1536 let settings_store = SettingsStore::test(cx);
1537 cx.set_global(settings_store);
1538 language::init(cx);
1539 TelemetrySettings::register(cx);
1540 agent_settings::AgentSettings::register(cx);
1541 Project::init_settings(cx);
1542 });
1543 }
1544}