1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Result};
5use assistant_tool::{ActionLog, Tool};
6use gpui::{App, Entity, Task};
7use itertools::Itertools;
8use language_model::LanguageModelRequestMessage;
9use project::Project;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use ui::IconName;
13use util::markdown::MarkdownString;
14
15#[derive(Debug, Serialize, Deserialize, JsonSchema)]
16pub struct ReadFileToolInput {
17 /// The relative path of the file to read.
18 ///
19 /// This path should never be absolute, and the first component
20 /// of the path should always be a root directory in a project.
21 ///
22 /// <example>
23 /// If the project has the following root directories:
24 ///
25 /// - directory1
26 /// - directory2
27 ///
28 /// If you wanna access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
29 /// If you wanna access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
30 /// </example>
31 pub path: Arc<Path>,
32
33 /// Optional line number to start reading on (1-based index)
34 #[serde(default)]
35 pub start_line: Option<usize>,
36
37 /// Optional line number to end reading on (1-based index)
38 #[serde(default)]
39 pub end_line: Option<usize>,
40}
41
42pub struct ReadFileTool;
43
44impl Tool for ReadFileTool {
45 fn name(&self) -> String {
46 "read-file".into()
47 }
48
49 fn needs_confirmation(&self) -> bool {
50 false
51 }
52
53 fn description(&self) -> String {
54 include_str!("./read_file_tool/description.md").into()
55 }
56
57 fn icon(&self) -> IconName {
58 IconName::Eye
59 }
60
61 fn input_schema(&self) -> serde_json::Value {
62 let schema = schemars::schema_for!(ReadFileToolInput);
63 serde_json::to_value(&schema).unwrap()
64 }
65
66 fn ui_text(&self, input: &serde_json::Value) -> String {
67 match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
68 Ok(input) => {
69 let path = MarkdownString::escape(&input.path.display().to_string());
70 format!("Read file `{path}`")
71 }
72 Err(_) => "Read file".to_string(),
73 }
74 }
75
76 fn run(
77 self: Arc<Self>,
78 input: serde_json::Value,
79 _messages: &[LanguageModelRequestMessage],
80 project: Entity<Project>,
81 action_log: Entity<ActionLog>,
82 cx: &mut App,
83 ) -> Task<Result<String>> {
84 let input = match serde_json::from_value::<ReadFileToolInput>(input) {
85 Ok(input) => input,
86 Err(err) => return Task::ready(Err(anyhow!(err))),
87 };
88
89 let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
90 return Task::ready(Err(anyhow!(
91 "Path {} not found in project",
92 &input.path.display()
93 )));
94 };
95
96 cx.spawn(async move |cx| {
97 let buffer = cx
98 .update(|cx| {
99 project.update(cx, |project, cx| project.open_buffer(project_path, cx))
100 })?
101 .await?;
102
103 let result = buffer.read_with(cx, |buffer, _cx| {
104 let text = buffer.text();
105 if input.start_line.is_some() || input.end_line.is_some() {
106 let start = input.start_line.unwrap_or(1);
107 let lines = text.split('\n').skip(start - 1);
108 if let Some(end) = input.end_line {
109 let count = end.saturating_sub(start);
110 Itertools::intersperse(lines.take(count), "\n").collect()
111 } else {
112 Itertools::intersperse(lines, "\n").collect()
113 }
114 } else {
115 text
116 }
117 })?;
118
119 action_log.update(cx, |log, cx| {
120 log.buffer_read(buffer, cx);
121 })?;
122
123 anyhow::Ok(result)
124 })
125 }
126}