Improve script tool description and add lines iterator to Lua file objects (#26529)

Antonio Scandurra and Agus Zubiaga created

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>

Change summary

crates/scripting_tool/src/scripting_session.rs           | 45 +++++++++
crates/scripting_tool/src/scripting_tool.rs              |  2 
crates/scripting_tool/src/scripting_tool_description.md  | 21 ++++
crates/scripting_tool/src/scripting_tool_description.txt | 22 ----
4 files changed, 66 insertions(+), 24 deletions(-)

Detailed changes

crates/scripting_tool/src/scripting_session.rs 🔗

@@ -7,7 +7,7 @@ use futures::{
 };
 use gpui::{AppContext, AsyncApp, Context, Entity, Task, WeakEntity};
 use language::Buffer;
-use mlua::{ExternalResult, Lua, MultiValue, Table, UserData, UserDataMethods};
+use mlua::{ExternalResult, Lua, MultiValue, ObjectLike, Table, UserData, UserDataMethods};
 use parking_lot::Mutex;
 use project::{search::SearchQuery, Fs, Project, ProjectPath, WorktreeId};
 use regex::Regex;
@@ -308,6 +308,10 @@ impl ScriptingSession {
         let read_fn = lua.create_function(Self::io_file_read)?;
         file.set("read", read_fn)?;
 
+        // lines method
+        let lines_fn = lua.create_function(Self::io_file_lines)?;
+        file.set("lines", lines_fn)?;
+
         // write method
         let write_fn = lua.create_function(Self::io_file_write)?;
         file.set("write", write_fn)?;
@@ -566,6 +570,17 @@ impl ScriptingSession {
         }
     }
 
+    fn io_file_lines(lua: &Lua, file_userdata: Table) -> mlua::Result<mlua::Function> {
+        let read_perm = file_userdata.get::<bool>("__read_perm")?;
+        if !read_perm {
+            return Err(mlua::Error::runtime("File not open for reading"));
+        }
+
+        lua.create_function::<_, _, mlua::Value>(move |lua, _: ()| {
+            file_userdata.call_method("read", lua.create_string("*l")?)
+        })
+    }
+
     fn io_file_read_format(format: Option<mlua::Value>) -> mlua::Result<FileReadFormat> {
         let format = match format {
             Some(mlua::Value::String(s)) => {
@@ -991,6 +1006,34 @@ mod tests {
         assert_eq!(test_session.diff(cx), Vec::new());
     }
 
+    #[gpui::test]
+    async fn test_lines_iterator(cx: &mut TestAppContext) {
+        let script = r#"
+            -- Create a test file with multiple lines
+            local file = io.open("lines_test.txt", "w")
+            file:write("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
+            file:close()
+
+            -- Read it back using the lines iterator
+            local read_file = io.open("lines_test.txt", "r")
+            local count = 0
+            for line in read_file:lines() do
+                count = count + 1
+                print(count .. ": " .. line)
+            end
+            read_file:close()
+
+            print("Total lines:", count)
+        "#;
+
+        let test_session = TestSession::init(cx).await;
+        let output = test_session.test_success(script, cx).await;
+        assert_eq!(
+            output,
+            "1: Line 1\n2: Line 2\n3: Line 3\n4: Line 4\n5: Line 5\nTotal lines:\t5\n"
+        );
+    }
+
     #[gpui::test]
     async fn test_read_write_roundtrip(cx: &mut TestAppContext) {
         let script = r#"

crates/scripting_tool/src/scripting_tool.rs 🔗

@@ -15,7 +15,7 @@ pub struct ScriptingTool;
 impl ScriptingTool {
     pub const NAME: &str = "lua-interpreter";
 
-    pub const DESCRIPTION: &str = include_str!("scripting_tool_description.txt");
+    pub const DESCRIPTION: &str = include_str!("scripting_tool_description.md");
 
     pub fn input_schema() -> serde_json::Value {
         let schema = schemars::schema_for!(ScriptingToolInput);

crates/scripting_tool/src/scripting_tool_description.md 🔗

@@ -0,0 +1,21 @@
+Evaluates the given Lua script in an interpreter with access to the Lua standard library. The tool returns the scripts output to stdout and any error that may have occurred.
+
+Use this tool to explore the current project and edit the user's codebase or operating system as requested.
+
+Additional functions provided:
+
+```lua
+--- Search for matches of a regular expression in files.
+-- @param pattern The regex pattern to search for (uses Rust's regex syntax)
+-- @return An array of tables with 'path' (file path) and 'matches' (array of matching strings)
+-- @usage local results = search("function\\s+\\w+")
+function search(pattern)
+  -- Implementation provided by the tool
+end
+
+--- Generates an outline for the given file path, extracting top-level symbols such as functions, classes, exports, and other significant declarations. This provides a structural overview of the file's contents.
+-- @param path
+function outline(path)
+  -- Implementation provided by the tool
+end
+```

crates/scripting_tool/src/scripting_tool_description.txt 🔗

@@ -1,22 +0,0 @@
-You can write a Lua script and I'll run it on my codebase and tell you what its
-output was, including both stdout as well as the git diff of changes it made to
-the filesystem. That way, you can get more information about the code base, or
-make changes to the code base directly.
-
-The Lua script will have access to `io` and it will run with the current working
-directory being in the root of the code base, so you can use it to explore,
-search, make changes, etc. You can also have the script print things, and I'll
-tell you what the output was. Note that `io` only has `open`, and then the file
-it returns only has the methods read, write, and close - it doesn't have popen
-or anything else.
-
-Also, I'm going to be putting this Lua script into JSON, so please don't use
-Lua's double quote syntax for string literals - use one of Lua's other syntaxes
-for string literals, so I don't have to escape the double quotes.
-
-There will be a global called `search` which accepts a regex (it's implemented
-using Rust's regex crate, so use that regex syntax) and runs that regex on the
-contents of every file in the code base (aside from gitignored files), then
-returns an array of tables with two fields: "path" (the path to the file that
-had the matches) and "matches" (an array of strings, with each string being a
-match that was found within the file).