Delete obsolete tools (#29808)

Richard Feldman and Cole Miller created

Release Notes:

- Removed some obsolete tools: batch_tool, code_actions, code_symbols,
contents, symbol_info, rename

Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

assets/settings/default.json                    |   6 
crates/assistant_tools/src/assistant_tools.rs   |  18 
crates/assistant_tools/src/batch_tool.rs        | 314 ---------------
crates/assistant_tools/src/code_action_tool.rs  | 388 -------------------
crates/assistant_tools/src/code_symbols_tool.rs | 247 ------------
crates/assistant_tools/src/contents_tool.rs     | 236 -----------
crates/assistant_tools/src/rename_tool.rs       | 204 ---------
crates/assistant_tools/src/symbol_info_tool.rs  | 307 ---------------
8 files changed, 1,720 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -683,10 +683,6 @@
         "name": "Write",
         "enable_all_context_servers": true,
         "tools": {
-          "batch_tool": false,
-          "code_actions": false,
-          "code_symbols": false,
-          "contents": false,
           "copy_path": false,
           "create_file": true,
           "delete_path": false,
@@ -699,8 +695,6 @@
           "find_path": true,
           "read_file": true,
           "grep": true,
-          "rename": false,
-          "symbol_info": false,
           "terminal": true,
           "thinking": true,
           "web_search": true

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -1,7 +1,3 @@
-mod batch_tool;
-mod code_action_tool;
-mod code_symbols_tool;
-mod contents_tool;
 mod copy_path_tool;
 mod create_directory_tool;
 mod create_file_tool;
@@ -17,11 +13,9 @@ mod move_path_tool;
 mod now_tool;
 mod open_tool;
 mod read_file_tool;
-mod rename_tool;
 mod replace;
 mod schema;
 mod streaming_edit_file_tool;
-mod symbol_info_tool;
 mod templates;
 mod terminal_tool;
 mod thinking_tool;
@@ -43,10 +37,6 @@ use web_search_tool::WebSearchTool;
 
 pub(crate) use templates::*;
 
-use crate::batch_tool::BatchTool;
-use crate::code_action_tool::CodeActionTool;
-use crate::code_symbols_tool::CodeSymbolsTool;
-use crate::contents_tool::ContentsTool;
 use crate::create_directory_tool::CreateDirectoryTool;
 use crate::delete_path_tool::DeletePathTool;
 use crate::diagnostics_tool::DiagnosticsTool;
@@ -56,9 +46,7 @@ use crate::grep_tool::GrepTool;
 use crate::list_directory_tool::ListDirectoryTool;
 use crate::now_tool::NowTool;
 use crate::read_file_tool::ReadFileTool;
-use crate::rename_tool::RenameTool;
 use crate::streaming_edit_file_tool::StreamingEditFileTool;
-use crate::symbol_info_tool::SymbolInfoTool;
 use crate::thinking_tool::ThinkingTool;
 
 pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
@@ -73,23 +61,17 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
 
     let registry = ToolRegistry::global(cx);
     registry.register_tool(TerminalTool);
-    registry.register_tool(BatchTool);
     registry.register_tool(CreateDirectoryTool);
     registry.register_tool(CopyPathTool);
     registry.register_tool(DeletePathTool);
-    registry.register_tool(SymbolInfoTool);
-    registry.register_tool(CodeActionTool);
     registry.register_tool(MovePathTool);
     registry.register_tool(DiagnosticsTool);
     registry.register_tool(ListDirectoryTool);
     registry.register_tool(NowTool);
     registry.register_tool(OpenTool);
-    registry.register_tool(CodeSymbolsTool);
-    registry.register_tool(ContentsTool);
     registry.register_tool(FindPathTool);
     registry.register_tool(ReadFileTool);
     registry.register_tool(GrepTool);
-    registry.register_tool(RenameTool);
     registry.register_tool(ThinkingTool);
     registry.register_tool(FetchTool::new(http_client));
 

crates/assistant_tools/src/batch_tool.rs 🔗

@@ -1,314 +0,0 @@
-use crate::schema::json_schema_for;
-use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
-use futures::future::join_all;
-use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::IconName;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct ToolInvocation {
-    /// The name of the tool to invoke
-    pub name: String,
-
-    /// The input to the tool in JSON format
-    pub input: serde_json::Value,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct BatchToolInput {
-    /// The tool invocations to run as a batch. These tools will be run either sequentially
-    /// or concurrently depending on the `run_tools_concurrently` flag.
-    ///
-    /// <example>
-    /// Basic file operations (concurrent)
-    ///
-    /// ```json
-    /// {
-    ///   "invocations": [
-    ///     {
-    ///       "name": "read_file",
-    ///       "input": {
-    ///         "path": "src/main.rs"
-    ///       }
-    ///     },
-    ///     {
-    ///       "name": "list_directory",
-    ///       "input": {
-    ///         "path": "src/lib"
-    ///       }
-    ///     },
-    ///     {
-    ///       "name": "grep",
-    ///       "input": {
-    ///         "regex": "fn run\\("
-    ///       }
-    ///     }
-    ///   ],
-    ///   "run_tools_concurrently": true
-    /// }
-    /// ```
-    /// </example>
-    ///
-    /// <example>
-    /// Multiple find-replace operations on the same file (sequential)
-    ///
-    /// ```json
-    /// {
-    ///   "invocations": [
-    ///     {
-    ///       "name": "find_replace_file",
-    ///       "input": {
-    ///         "path": "src/config.rs",
-    ///         "display_description": "Update default timeout value",
-    ///         "find": "pub const DEFAULT_TIMEOUT: u64 = 30;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";",
-    ///         "replace": "pub const DEFAULT_TIMEOUT: u64 = 60;\n\npub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";"
-    ///       }
-    ///     },
-    ///     {
-    ///       "name": "find_replace_file",
-    ///       "input": {
-    ///         "path": "src/config.rs",
-    ///         "display_description": "Update API endpoint URL",
-    ///         "find": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.example.com\";\n\npub const API_VERSION: &str = \"v1\";",
-    ///         "replace": "pub const MAX_RETRIES: u32 = 3;\n\npub const SERVER_URL: &str = \"https://api.newdomain.com\";\n\npub const API_VERSION: &str = \"v1\";"
-    ///       }
-    ///     }
-    ///   ],
-    ///   "run_tools_concurrently": false
-    /// }
-    /// ```
-    /// </example>
-    ///
-    /// <example>
-    /// Searching and analyzing code (concurrent)
-    ///
-    /// ```json
-    /// {
-    ///   "invocations": [
-    ///     {
-    ///       "name": "grep",
-    ///       "input": {
-    ///         "regex": "impl Database"
-    ///       }
-    ///     },
-    ///     {
-    ///       "name": "find_path",
-    ///       "input": {
-    ///         "glob": "**/*test*.rs"
-    ///       }
-    ///     }
-    ///   ],
-    ///   "run_tools_concurrently": true
-    /// }
-    /// ```
-    /// </example>
-    ///
-    /// <example>
-    /// Multi-file refactoring (concurrent)
-    ///
-    /// ```json
-    /// {
-    ///   "invocations": [
-    ///     {
-    ///       "name": "find_replace_file",
-    ///       "input": {
-    ///         "path": "src/models/user.rs",
-    ///         "display_description": "Add email field to User struct",
-    ///         "find": "pub struct User {\n    pub id: u64,\n    pub username: String,\n    pub created_at: DateTime<Utc>,\n}",
-    ///         "replace": "pub struct User {\n    pub id: u64,\n    pub username: String,\n    pub email: String,\n    pub created_at: DateTime<Utc>,\n}"
-    ///       }
-    ///     },
-    ///     {
-    ///       "name": "find_replace_file",
-    ///       "input": {
-    ///         "path": "src/db/queries.rs",
-    ///         "display_description": "Update user insertion query",
-    ///         "find": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n    conn.execute(\n        \"INSERT INTO users (id, username, created_at) VALUES ($1, $2, $3)\",\n        &[&user.id, &user.username, &user.created_at],\n    ).await?;\n    \n    Ok(())\n}",
-    ///         "replace": "pub async fn insert_user(conn: &mut Connection, user: &User) -> Result<(), DbError> {\n    conn.execute(\n        \"INSERT INTO users (id, username, email, created_at) VALUES ($1, $2, $3, $4)\",\n        &[&user.id, &user.username, &user.email, &user.created_at],\n    ).await?;\n    \n    Ok(())\n}"
-    ///       }
-    ///     }
-    ///   ],
-    ///   "run_tools_concurrently": true
-    /// }
-    /// ```
-    /// </example>
-    pub invocations: Vec<ToolInvocation>,
-
-    /// Whether to run the tools in this batch concurrently. If this is false (the default), the tools will run sequentially.
-    #[serde(default)]
-    pub run_tools_concurrently: bool,
-}
-
-pub struct BatchTool;
-
-impl Tool for BatchTool {
-    fn name(&self) -> String {
-        "batch_tool".into()
-    }
-
-    fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool {
-        serde_json::from_value::<BatchToolInput>(input.clone())
-            .map(|input| {
-                let working_set = ToolWorkingSet::default();
-                input.invocations.iter().any(|invocation| {
-                    working_set
-                        .tool(&invocation.name, cx)
-                        .map_or(false, |tool| tool.needs_confirmation(&invocation.input, cx))
-                })
-            })
-            .unwrap_or(false)
-    }
-
-    fn description(&self) -> String {
-        include_str!("./batch_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Cog
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<BatchToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<BatchToolInput>(input.clone()) {
-            Ok(input) => {
-                let count = input.invocations.len();
-                let mode = if input.run_tools_concurrently {
-                    "concurrently"
-                } else {
-                    "sequentially"
-                };
-
-                let first_tool_name = input
-                    .invocations
-                    .first()
-                    .map(|inv| inv.name.clone())
-                    .unwrap_or_default();
-
-                let all_same = input
-                    .invocations
-                    .iter()
-                    .all(|invocation| invocation.name == first_tool_name);
-
-                if all_same {
-                    format!(
-                        "Run `{}` {} times {}",
-                        first_tool_name,
-                        input.invocations.len(),
-                        mode
-                    )
-                } else {
-                    format!("Run {} tools {}", count, mode)
-                }
-            }
-            Err(_) => "Batch tools".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<BatchToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        if input.invocations.is_empty() {
-            return Task::ready(Err(anyhow!("No tool invocations provided"))).into();
-        }
-
-        let run_tools_concurrently = input.run_tools_concurrently;
-
-        let foreground_task = {
-            let working_set = ToolWorkingSet::default();
-            let invocations = input.invocations;
-            let messages = messages.to_vec();
-
-            cx.spawn(async move |cx| {
-                let mut tasks = Vec::new();
-                let mut tool_names = Vec::new();
-
-                for invocation in invocations {
-                    let tool_name = invocation.name.clone();
-                    tool_names.push(tool_name.clone());
-
-                    let tool = cx
-                        .update(|cx| working_set.tool(&tool_name, cx))
-                        .map_err(|err| {
-                            anyhow!("Failed to look up tool '{}': {}", tool_name, err)
-                        })?;
-
-                    let Some(tool) = tool else {
-                        return Err(anyhow!("Tool '{}' not found", tool_name));
-                    };
-
-                    let project = project.clone();
-                    let action_log = action_log.clone();
-                    let messages = messages.clone();
-                    let tool_result = cx
-                        .update(|cx| {
-                            tool.run(invocation.input, &messages, project, action_log, window, cx)
-                        })
-                        .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
-
-                    tasks.push(tool_result.output);
-                }
-
-                Ok((tasks, tool_names))
-            })
-        };
-
-        cx.background_spawn(async move {
-            let (tasks, tool_names) = foreground_task.await?;
-            let mut results = Vec::with_capacity(tasks.len());
-
-            if run_tools_concurrently {
-                results.extend(join_all(tasks).await)
-            } else {
-                for task in tasks {
-                    results.push(task.await);
-                }
-            };
-
-            let mut formatted_results = String::new();
-            let mut error_occurred = false;
-
-            for (i, result) in results.into_iter().enumerate() {
-                let tool_name = &tool_names[i];
-
-                match result {
-                    Ok(output) => {
-                        formatted_results
-                            .push_str(&format!("Tool '{}' result:\n{}\n\n", tool_name, output));
-                    }
-                    Err(err) => {
-                        error_occurred = true;
-                        formatted_results
-                            .push_str(&format!("Tool '{}' error: {}\n\n", tool_name, err));
-                    }
-                }
-            }
-
-            if error_occurred {
-                formatted_results
-                    .push_str("Note: Some tool invocations failed. See individual results above.");
-            }
-
-            Ok(formatted_results.trim().to_string())
-        })
-        .into()
-    }
-}

crates/assistant_tools/src/code_action_tool.rs 🔗

@@ -1,388 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language::{self, Anchor, Buffer, ToPointUtf16};
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::{self, LspAction, Project};
-use regex::Regex;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{ops::Range, sync::Arc};
-use ui::IconName;
-
-use crate::schema::json_schema_for;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct CodeActionToolInput {
-    /// The relative path to the file containing the text range.
-    ///
-    /// WARNING: you MUST start this path with one of the project's root directories.
-    pub path: String,
-
-    /// The specific code action to execute.
-    ///
-    /// If this field is provided, the tool will execute the specified action.
-    /// If omitted, the tool will list all available code actions for the text range.
-    ///
-    /// Here are some actions that are commonly supported (but may not be for this particular
-    /// text range; you can omit this field to list all the actions, if you want to know
-    /// what your options are, or you can just try an action and if it fails I'll tell you
-    /// what the available actions were instead):
-    /// - "quickfix.all" - applies all available quick fixes in the range
-    /// - "source.organizeImports" - sorts and cleans up import statements
-    /// - "source.fixAll" - applies all available auto fixes
-    /// - "refactor.extract" - extracts selected code into a new function or variable
-    /// - "refactor.inline" - inlines a variable by replacing references with its value
-    /// - "refactor.rewrite" - general code rewriting operations
-    /// - "source.addMissingImports" - adds imports for references that lack them
-    /// - "source.removeUnusedImports" - removes imports that aren't being used
-    /// - "source.implementInterface" - generates methods required by an interface/trait
-    /// - "source.generateAccessors" - creates getter/setter methods
-    /// - "source.convertToAsyncFunction" - converts callback-style code to async/await
-    ///
-    /// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
-    /// then this will rename the symbol to whatever string you specified for the `arguments` field.
-    pub action: Option<String>,
-
-    /// Optional arguments to pass to the code action.
-    ///
-    /// For rename operations (when action="textDocument/rename"), this should contain the new name.
-    /// For other code actions, these arguments may be passed to the language server.
-    pub arguments: Option<serde_json::Value>,
-
-    /// The text that comes immediately before the text range in the file.
-    pub context_before_range: String,
-
-    /// The text range. This text must appear in the file right between `context_before_range`
-    /// and `context_after_range`.
-    ///
-    /// The file must contain exactly one occurrence of `context_before_range` followed by
-    /// `text_range` followed by `context_after_range`. If the file contains zero occurrences,
-    /// or if it contains more than one occurrence, the tool will fail, so it is absolutely
-    /// critical that you verify ahead of time that the string is unique. You can search
-    /// the file's contents to verify this ahead of time.
-    ///
-    /// To make the string more likely to be unique, include a minimum of 1 line of context
-    /// before the text range, as well as a minimum of 1 line of context after the text range.
-    /// If these lines of context are not enough to obtain a string that appears only once
-    /// in the file, then double the number of context lines until the string becomes unique.
-    /// (Start with 1 line before and 1 line after though, because too much context is
-    /// needlessly costly.)
-    ///
-    /// Do not alter the context lines of code in any way, and make sure to preserve all
-    /// whitespace and indentation for all lines of code. The combined string must be exactly
-    /// as it appears in the file, or else this tool call will fail.
-    pub text_range: String,
-
-    /// The text that comes immediately after the text range in the file.
-    pub context_after_range: String,
-}
-
-pub struct CodeActionTool;
-
-impl Tool for CodeActionTool {
-    fn name(&self) -> String {
-        "code_actions".into()
-    }
-
-    fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./code_action_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Wand
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<CodeActionToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<CodeActionToolInput>(input.clone()) {
-            Ok(input) => {
-                if let Some(action) = &input.action {
-                    if action == "textDocument/rename" {
-                        let new_name = match &input.arguments {
-                            Some(serde_json::Value::String(new_name)) => new_name.clone(),
-                            Some(value) => {
-                                if let Ok(new_name) =
-                                    serde_json::from_value::<String>(value.clone())
-                                {
-                                    new_name
-                                } else {
-                                    "invalid name".to_string()
-                                }
-                            }
-                            None => "missing name".to_string(),
-                        };
-                        format!("Rename '{}' to '{}'", input.text_range, new_name)
-                    } else {
-                        format!(
-                            "Execute code action '{}' for '{}'",
-                            action, input.text_range
-                        )
-                    }
-                } else {
-                    format!("List available code actions for '{}'", input.text_range)
-                }
-            }
-            Err(_) => "Perform code action".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<CodeActionToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        cx.spawn(async move |cx| {
-            let buffer = {
-                let project_path = project.read_with(cx, |project, cx| {
-                    project
-                        .find_project_path(&input.path, cx)
-                        .context("Path not found in project")
-                })??;
-
-                project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
-            };
-
-            action_log.update(cx, |action_log, cx| {
-                action_log.track_buffer(buffer.clone(), cx);
-            })?;
-
-            let range = {
-                let Some(range) = buffer.read_with(cx, |buffer, _cx| {
-                    find_text_range(&buffer, &input.context_before_range, &input.text_range, &input.context_after_range)
-                })? else {
-                    return Err(anyhow!(
-                        "Failed to locate the text specified by context_before_range, text_range, and context_after_range. Make sure context_before_range and context_after_range each match exactly once in the file."
-                    ));
-                };
-
-                range
-            };
-
-            if let Some(action_type) = &input.action {
-                // Special-case the `rename` operation
-                let response = if action_type == "textDocument/rename" {
-                    let Some(new_name) = input.arguments.and_then(|args| serde_json::from_value::<String>(args).ok()) else {
-                        return Err(anyhow!("For rename operations, 'arguments' must be a string containing the new name"));
-                    };
-
-                    let position = buffer.read_with(cx, |buffer, _| {
-                        range.start.to_point_utf16(&buffer.snapshot())
-                    })?;
-
-                    project
-                        .update(cx, |project, cx| {
-                            project.perform_rename(buffer.clone(), position, new_name.clone(), cx)
-                        })?
-                        .await?;
-
-                    format!("Renamed '{}' to '{}'", input.text_range, new_name)
-                } else {
-                    // Get code actions for the range
-                    let actions = project
-                        .update(cx, |project, cx| {
-                            project.code_actions(&buffer, range.clone(), None, cx)
-                        })?
-                        .await?;
-
-                    if actions.is_empty() {
-                        return Err(anyhow!("No code actions available for this range"));
-                    }
-
-                    // Find all matching actions
-                    let regex = match Regex::new(action_type) {
-                        Ok(regex) => regex,
-                        Err(err) => return Err(anyhow!("Invalid regex pattern: {}", err)),
-                    };
-                    let mut matching_actions = actions
-                        .into_iter()
-                        .filter(|action| { regex.is_match(action.lsp_action.title()) });
-
-                    let Some(action) = matching_actions.next() else {
-                        return Err(anyhow!("No code actions match the pattern: {}", action_type));
-                    };
-
-                    // There should have been exactly one matching action.
-                    if let Some(second) = matching_actions.next() {
-                        let mut all_matches = vec![action, second];
-
-                        all_matches.extend(matching_actions);
-
-                        return Err(anyhow!(
-                            "Pattern '{}' matches multiple code actions: {}",
-                            action_type,
-                            all_matches.into_iter().map(|action| action.lsp_action.title().to_string()).collect::<Vec<_>>().join(", ")
-                        ));
-                    }
-
-                    let title = action.lsp_action.title().to_string();
-
-                    project
-                        .update(cx, |project, cx| {
-                            project.apply_code_action(buffer.clone(), action, true, cx)
-                        })?
-                        .await?;
-
-                    format!("Completed code action: {}", title)
-                };
-
-                project
-                    .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
-                    .await?;
-
-                action_log.update(cx, |log, cx| {
-                    log.buffer_edited(buffer.clone(), cx)
-                })?;
-
-                Ok(response)
-            } else {
-                // No action specified, so list the available ones.
-                let (position_start, position_end) = buffer.read_with(cx, |buffer, _| {
-                    let snapshot = buffer.snapshot();
-                    (
-                        range.start.to_point_utf16(&snapshot),
-                        range.end.to_point_utf16(&snapshot)
-                    )
-                })?;
-
-                // Convert position to display coordinates (1-based)
-                let position_start_display = language::Point {
-                    row: position_start.row + 1,
-                    column: position_start.column + 1,
-                };
-
-                let position_end_display = language::Point {
-                    row: position_end.row + 1,
-                    column: position_end.column + 1,
-                };
-
-                // Get code actions for the range
-                let actions = project
-                    .update(cx, |project, cx| {
-                        project.code_actions(&buffer, range.clone(), None, cx)
-                    })?
-                    .await?;
-
-                let mut response = format!(
-                    "Available code actions for text range '{}' at position {}:{} to {}:{} (UTF-16 coordinates):\n\n",
-                    input.text_range,
-                    position_start_display.row, position_start_display.column,
-                    position_end_display.row, position_end_display.column
-                );
-
-                if actions.is_empty() {
-                    response.push_str("No code actions available for this range.");
-                } else {
-                    for (i, action) in actions.iter().enumerate() {
-                        let title = match &action.lsp_action {
-                            LspAction::Action(code_action) => code_action.title.as_str(),
-                            LspAction::Command(command) => command.title.as_str(),
-                            LspAction::CodeLens(code_lens) => {
-                                if let Some(cmd) = &code_lens.command {
-                                    cmd.title.as_str()
-                                } else {
-                                    "Unknown code lens"
-                                }
-                            },
-                        };
-
-                        let kind = match &action.lsp_action {
-                            LspAction::Action(code_action) => {
-                                if let Some(kind) = &code_action.kind {
-                                    kind.as_str()
-                                } else {
-                                    "unknown"
-                                }
-                            },
-                            LspAction::Command(_) => "command",
-                            LspAction::CodeLens(_) => "code_lens",
-                        };
-
-                        response.push_str(&format!("{}. {title} ({kind})\n", i + 1));
-                    }
-                }
-
-                Ok(response)
-            }
-        }).into()
-    }
-}
-
-/// Finds the range of the text in the buffer, if it appears between context_before_range
-/// and context_after_range, and if that combined string has one unique result in the buffer.
-///
-/// If an exact match fails, it tries adding a newline to the end of context_before_range and
-/// to the beginning of context_after_range to accommodate line-based context matching.
-fn find_text_range(
-    buffer: &Buffer,
-    context_before_range: &str,
-    text_range: &str,
-    context_after_range: &str,
-) -> Option<Range<Anchor>> {
-    let snapshot = buffer.snapshot();
-    let text = snapshot.text();
-
-    // First try with exact match
-    let search_string = format!("{context_before_range}{text_range}{context_after_range}");
-    let mut positions = text.match_indices(&search_string);
-    let position_result = positions.next();
-
-    if let Some(position) = position_result {
-        // Check if the matched string is unique
-        if positions.next().is_none() {
-            let range_start = position.0 + context_before_range.len();
-            let range_end = range_start + text_range.len();
-            let range_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_start));
-            let range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(range_end));
-
-            return Some(range_start_anchor..range_end_anchor);
-        }
-    }
-
-    // If exact match fails or is not unique, try with line-based context
-    // Add a newline to the end of before context and beginning of after context
-    let line_based_before = if context_before_range.ends_with('\n') {
-        context_before_range.to_string()
-    } else {
-        format!("{context_before_range}\n")
-    };
-
-    let line_based_after = if context_after_range.starts_with('\n') {
-        context_after_range.to_string()
-    } else {
-        format!("\n{context_after_range}")
-    };
-
-    let line_search_string = format!("{line_based_before}{text_range}{line_based_after}");
-    let mut line_positions = text.match_indices(&line_search_string);
-    let line_position = line_positions.next()?;
-
-    // The line-based search string must also appear exactly once
-    if line_positions.next().is_some() {
-        return None;
-    }
-
-    let line_range_start = line_position.0 + line_based_before.len();
-    let line_range_end = line_range_start + text_range.len();
-    let line_range_start_anchor =
-        snapshot.anchor_before(snapshot.offset_to_point(line_range_start));
-    let line_range_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(line_range_end));
-
-    Some(line_range_start_anchor..line_range_end_anchor)
-}

crates/assistant_tools/src/code_symbols_tool.rs 🔗

@@ -1,247 +0,0 @@
-use std::fmt::Write;
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use crate::schema::json_schema_for;
-use anyhow::{Result, anyhow};
-use assistant_tool::outline;
-use assistant_tool::{ActionLog, Tool, ToolResult};
-use collections::IndexMap;
-use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::{Project, Symbol};
-use regex::{Regex, RegexBuilder};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct CodeSymbolsInput {
-    /// The relative path of the source code file to read and get the symbols for.
-    /// This tool should only be used on source code files, never on any other type of file.
-    ///
-    /// This path should never be absolute, and the first component
-    /// of the path should always be a root directory in a project.
-    ///
-    /// If no path is specified, this tool returns a flat list of all symbols in the project
-    /// instead of a hierarchical outline of a specific file.
-    ///
-    /// <example>
-    /// If the project has the following root directories:
-    ///
-    /// - directory1
-    /// - directory2
-    ///
-    /// If you want to access `file.md` in `directory1`, you should use the path `directory1/file.md`.
-    /// If you want to access `file.md` in `directory2`, you should use the path `directory2/file.md`.
-    /// </example>
-    #[serde(default)]
-    pub path: Option<String>,
-
-    /// Optional regex pattern to filter symbols by name.
-    /// When provided, only symbols whose names match this pattern will be included in the results.
-    ///
-    /// <example>
-    /// To find only symbols that contain the word "test", use the regex pattern "test".
-    /// To find methods that start with "get_", use the regex pattern "^get_".
-    /// </example>
-    #[serde(default)]
-    pub regex: Option<String>,
-
-    /// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
-    ///
-    /// <example>
-    /// Set to `true` to make regex matching case-sensitive.
-    /// </example>
-    #[serde(default)]
-    pub case_sensitive: bool,
-
-    /// Optional starting position for paginated results (0-based).
-    /// When not provided, starts from the beginning.
-    #[serde(default)]
-    pub offset: u32,
-}
-
-impl CodeSymbolsInput {
-    /// Which page of search results this is.
-    pub fn page(&self) -> u32 {
-        1 + (self.offset / RESULTS_PER_PAGE)
-    }
-}
-
-const RESULTS_PER_PAGE: u32 = 2000;
-
-pub struct CodeSymbolsTool;
-
-impl Tool for CodeSymbolsTool {
-    fn name(&self) -> String {
-        "code_symbols".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./code_symbols_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Code
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<CodeSymbolsInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<CodeSymbolsInput>(input.clone()) {
-            Ok(input) => {
-                let page = input.page();
-
-                match &input.path {
-                    Some(path) => {
-                        let path = MarkdownInlineCode(path);
-                        if page > 1 {
-                            format!("List page {page} of code symbols for {path}")
-                        } else {
-                            format!("List code symbols for {path}")
-                        }
-                    }
-                    None => {
-                        if page > 1 {
-                            format!("List page {page} of project symbols")
-                        } else {
-                            "List all project symbols".to_string()
-                        }
-                    }
-                }
-            }
-            Err(_) => "List code symbols".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        let regex = match input.regex {
-            Some(regex_str) => match RegexBuilder::new(&regex_str)
-                .case_insensitive(!input.case_sensitive)
-                .build()
-            {
-                Ok(regex) => Some(regex),
-                Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))).into(),
-            },
-            None => None,
-        };
-
-        cx.spawn(async move |cx| match input.path {
-            Some(path) => outline::file_outline(project, path, action_log, regex, cx).await,
-            None => project_symbols(project, regex, input.offset, cx).await,
-        })
-        .into()
-    }
-}
-
-async fn project_symbols(
-    project: Entity<Project>,
-    regex: Option<Regex>,
-    offset: u32,
-    cx: &mut AsyncApp,
-) -> anyhow::Result<String> {
-    let symbols = project
-        .update(cx, |project, cx| project.symbols("", cx))?
-        .await?;
-
-    if symbols.is_empty() {
-        return Err(anyhow!("No symbols found in project."));
-    }
-
-    let mut symbols_by_path: IndexMap<PathBuf, Vec<&Symbol>> = IndexMap::default();
-
-    for symbol in symbols
-        .iter()
-        .filter(|symbol| {
-            if let Some(regex) = &regex {
-                regex.is_match(&symbol.name)
-            } else {
-                true
-            }
-        })
-        .skip(offset as usize)
-        // Take 1 more than RESULTS_PER_PAGE so we can tell if there are more results.
-        .take((RESULTS_PER_PAGE as usize).saturating_add(1))
-    {
-        if let Some(worktree_path) = project.read_with(cx, |project, cx| {
-            project
-                .worktree_for_id(symbol.path.worktree_id, cx)
-                .map(|worktree| PathBuf::from(worktree.read(cx).root_name()))
-        })? {
-            let path = worktree_path.join(&symbol.path.path);
-            symbols_by_path.entry(path).or_default().push(symbol);
-        }
-    }
-
-    // If no symbols matched the filter, return early
-    if symbols_by_path.is_empty() {
-        return Err(anyhow!("No symbols found matching the criteria."));
-    }
-
-    let mut symbols_rendered = 0;
-    let mut has_more_symbols = false;
-    let mut output = String::new();
-
-    'outer: for (file_path, file_symbols) in symbols_by_path {
-        if symbols_rendered > 0 {
-            output.push('\n');
-        }
-
-        writeln!(&mut output, "{}", file_path.display()).ok();
-
-        for symbol in file_symbols {
-            if symbols_rendered >= RESULTS_PER_PAGE {
-                has_more_symbols = true;
-                break 'outer;
-            }
-
-            write!(&mut output, "  {} ", symbol.label.text()).ok();
-
-            // Convert to 1-based line numbers for display
-            let start_line = symbol.range.start.0.row as usize + 1;
-            let end_line = symbol.range.end.0.row as usize + 1;
-
-            if start_line == end_line {
-                writeln!(&mut output, "[L{}]", start_line).ok();
-            } else {
-                writeln!(&mut output, "[L{}-{}]", start_line, end_line).ok();
-            }
-
-            symbols_rendered += 1;
-        }
-    }
-
-    Ok(if symbols_rendered == 0 {
-        "No symbols found in the requested page.".to_string()
-    } else if has_more_symbols {
-        format!(
-            "{output}\nShowing symbols {}-{} (more symbols were found; use offset: {} to see next page)",
-            offset + 1,
-            offset + symbols_rendered,
-            offset + RESULTS_PER_PAGE,
-        )
-    } else {
-        output
-    })
-}

crates/assistant_tools/src/contents_tool.rs 🔗

@@ -1,236 +0,0 @@
-use std::sync::Arc;
-
-use crate::schema::json_schema_for;
-use anyhow::{Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult, outline};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use itertools::Itertools;
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{fmt::Write, path::Path};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-/// If the model requests to read a file whose size exceeds this, then
-/// If the model requests to list the entries in a directory with more
-/// entries than this, then the tool will return a subset of the entries
-/// and suggest trying again.
-const MAX_DIR_ENTRIES: usize = 1024;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct ContentsToolInput {
-    /// The relative path of the file or directory to access.
-    ///
-    /// This path should never be absolute, and the first component
-    /// of the path should always be a root directory in a project.
-    ///
-    /// <example>
-    /// If the project has the following root directories:
-    ///
-    /// - directory1
-    /// - directory2
-    ///
-    /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
-    /// If you want to list contents in the directory `directory2/subfolder`, you should use the path `directory2/subfolder`.
-    /// </example>
-    pub path: String,
-
-    /// Optional position (1-based index) to start reading on, if you want to read a subset of the contents.
-    /// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
-    /// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
-    ///
-    /// Defaults to 1.
-    pub start: Option<u32>,
-
-    /// Optional position (1-based index) to end reading on, if you want to read a subset of the contents.
-    /// When reading a file, this refers to a line number in the file (e.g. 1 is the first line).
-    /// When reading a directory, this refers to the number of the directory entry (e.g. 1 is the first entry).
-    ///
-    /// Defaults to reading until the end of the file or directory.
-    pub end: Option<u32>,
-}
-
-pub struct ContentsTool;
-
-impl Tool for ContentsTool {
-    fn name(&self) -> String {
-        "contents".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./contents_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::FileSearch
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<ContentsToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<ContentsToolInput>(input.clone()) {
-            Ok(input) => {
-                let path = MarkdownInlineCode(&input.path);
-
-                match (input.start, input.end) {
-                    (Some(start), None) => format!("Read {path} (from line {start})"),
-                    (Some(start), Some(end)) => {
-                        format!("Read {path} (lines {start}-{end})")
-                    }
-                    _ => format!("Read {path}"),
-                }
-            }
-            Err(_) => "Read file or directory".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<ContentsToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        // Sometimes models will return these even though we tell it to give a path and not a glob.
-        // When this happens, just list the root worktree directories.
-        if matches!(input.path.as_str(), "." | "" | "./" | "*") {
-            let output = project
-                .read(cx)
-                .worktrees(cx)
-                .filter_map(|worktree| {
-                    worktree.read(cx).root_entry().and_then(|entry| {
-                        if entry.is_dir() {
-                            entry.path.to_str()
-                        } else {
-                            None
-                        }
-                    })
-                })
-                .collect::<Vec<_>>()
-                .join("\n");
-
-            return Task::ready(Ok(output)).into();
-        }
-
-        let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
-            return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
-        };
-
-        let Some(worktree) = project
-            .read(cx)
-            .worktree_for_id(project_path.worktree_id, cx)
-        else {
-            return Task::ready(Err(anyhow!("Worktree not found"))).into();
-        };
-        let worktree = worktree.read(cx);
-
-        let Some(entry) = worktree.entry_for_path(&project_path.path) else {
-            return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
-        };
-
-        // If it's a directory, list its contents
-        if entry.is_dir() {
-            let mut output = String::new();
-            let start_index = input
-                .start
-                .map(|line| (line as usize).saturating_sub(1))
-                .unwrap_or(0);
-            let end_index = input
-                .end
-                .map(|line| (line as usize).saturating_sub(1))
-                .unwrap_or(MAX_DIR_ENTRIES);
-            let mut skipped = 0;
-
-            for (index, entry) in worktree.child_entries(&project_path.path).enumerate() {
-                if index >= start_index && index <= end_index {
-                    writeln!(
-                        output,
-                        "{}",
-                        Path::new(worktree.root_name()).join(&entry.path).display(),
-                    )
-                    .unwrap();
-                } else {
-                    skipped += 1;
-                }
-            }
-
-            if output.is_empty() {
-                output.push_str(&input.path);
-                output.push_str(" is empty.");
-            }
-
-            if skipped > 0 {
-                write!(
-                    output,
-                    "\n\nNote: Skipped {skipped} entries. Adjust start and end to see other entries.",
-                ).ok();
-            }
-
-            Task::ready(Ok(output)).into()
-        } else {
-            // It's a file, so read its contents
-            let file_path = input.path.clone();
-            cx.spawn(async move |cx| {
-                let buffer = cx
-                    .update(|cx| {
-                        project.update(cx, |project, cx| project.open_buffer(project_path, cx))
-                    })?
-                    .await?;
-
-                if input.start.is_some() || input.end.is_some() {
-                    let result = buffer.read_with(cx, |buffer, _cx| {
-                        let text = buffer.text();
-                        let start = input.start.unwrap_or(1);
-                        let lines = text.split('\n').skip(start as usize - 1);
-                        if let Some(end) = input.end {
-                            let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
-                            Itertools::intersperse(lines.take(count as usize), "\n").collect()
-                        } else {
-                            Itertools::intersperse(lines, "\n").collect()
-                        }
-                    })?;
-
-                    action_log.update(cx, |log, cx| {
-                        log.track_buffer(buffer, cx);
-                    })?;
-
-                    Ok(result)
-                } else {
-                    // No line ranges specified, so check file size to see if it's too big.
-                    let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
-
-                    if file_size <= outline::AUTO_OUTLINE_SIZE {
-                        let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-
-                        action_log.update(cx, |log, cx| {
-                            log.track_buffer(buffer, cx);
-                        })?;
-
-                        Ok(result)
-                    } else {
-                        // File is too big, so return its outline and a suggestion to
-                        // read again with a line number range specified.
-                        let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
-
-                        Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
-                    }
-                }
-            }).into()
-        }
-    }
-}

crates/assistant_tools/src/rename_tool.rs 🔗

@@ -1,204 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, Entity, Task};
-use language::{self, Buffer, ToPointUtf16};
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use ui::IconName;
-
-use crate::schema::json_schema_for;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct RenameToolInput {
-    /// The relative path to the file containing the symbol to rename.
-    ///
-    /// WARNING: you MUST start this path with one of the project's root directories.
-    pub path: String,
-
-    /// The new name to give to the symbol.
-    pub new_name: String,
-
-    /// The text that comes immediately before the symbol in the file.
-    pub context_before_symbol: String,
-
-    /// The symbol to rename. This text must appear in the file right between
-    /// `context_before_symbol` and `context_after_symbol`.
-    ///
-    /// The file must contain exactly one occurrence of `context_before_symbol` followed by
-    /// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
-    /// or if it contains more than one occurrence, the tool will fail, so it is absolutely
-    /// critical that you verify ahead of time that the string is unique. You can search
-    /// the file's contents to verify this ahead of time.
-    ///
-    /// To make the string more likely to be unique, include a minimum of 1 line of context
-    /// before the symbol, as well as a minimum of 1 line of context after the symbol.
-    /// If these lines of context are not enough to obtain a string that appears only once
-    /// in the file, then double the number of context lines until the string becomes unique.
-    /// (Start with 1 line before and 1 line after though, because too much context is
-    /// needlessly costly.)
-    ///
-    /// Do not alter the context lines of code in any way, and make sure to preserve all
-    /// whitespace and indentation for all lines of code. The combined string must be exactly
-    /// as it appears in the file, or else this tool call will fail.
-    pub symbol: String,
-
-    /// The text that comes immediately after the symbol in the file.
-    pub context_after_symbol: String,
-}
-
-pub struct RenameTool;
-
-impl Tool for RenameTool {
-    fn name(&self) -> String {
-        "rename".into()
-    }
-
-    fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./rename_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Pencil
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<RenameToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<RenameToolInput>(input.clone()) {
-            Ok(input) => {
-                format!("Rename '{}' to '{}'", input.symbol, input.new_name)
-            }
-            Err(_) => "Rename symbol".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<RenameToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        cx.spawn(async move |cx| {
-            let buffer = {
-                let project_path = project.read_with(cx, |project, cx| {
-                    project
-                        .find_project_path(&input.path, cx)
-                        .context("Path not found in project")
-                })??;
-
-                project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
-            };
-
-            action_log.update(cx, |action_log, cx| {
-                action_log.track_buffer(buffer.clone(), cx);
-            })?;
-
-            let position = {
-                let Some(position) = buffer.read_with(cx, |buffer, _cx| {
-                    find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
-                })? else {
-                    return Err(anyhow!(
-                        "Failed to locate the symbol specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
-                    ));
-                };
-
-                buffer.read_with(cx, |buffer, _| {
-                    position.to_point_utf16(&buffer.snapshot())
-                })?
-            };
-
-            project
-                .update(cx, |project, cx| {
-                    project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)
-                })?
-                .await?;
-
-            project
-                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
-                .await?;
-
-            action_log.update(cx, |log, cx| {
-                log.buffer_edited(buffer.clone(), cx)
-            })?;
-
-            Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
-        }).into()
-    }
-}
-
-/// Finds the position of the symbol in the buffer, if it appears between context_before_symbol
-/// and context_after_symbol, and if that combined string has one unique result in the buffer.
-///
-/// If an exact match fails, it tries adding a newline to the end of context_before_symbol and
-/// to the beginning of context_after_symbol to accommodate line-based context matching.
-fn find_symbol_position(
-    buffer: &Buffer,
-    context_before_symbol: &str,
-    symbol: &str,
-    context_after_symbol: &str,
-) -> Option<language::Anchor> {
-    let snapshot = buffer.snapshot();
-    let text = snapshot.text();
-
-    // First try with exact match
-    let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
-    let mut positions = text.match_indices(&search_string);
-    let position_result = positions.next();
-
-    if let Some(position) = position_result {
-        // Check if the matched string is unique
-        if positions.next().is_none() {
-            let symbol_start = position.0 + context_before_symbol.len();
-            let symbol_start_anchor =
-                snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
-
-            return Some(symbol_start_anchor);
-        }
-    }
-
-    // If exact match fails or is not unique, try with line-based context
-    // Add a newline to the end of before context and beginning of after context
-    let line_based_before = if context_before_symbol.ends_with('\n') {
-        context_before_symbol.to_string()
-    } else {
-        format!("{context_before_symbol}\n")
-    };
-
-    let line_based_after = if context_after_symbol.starts_with('\n') {
-        context_after_symbol.to_string()
-    } else {
-        format!("\n{context_after_symbol}")
-    };
-
-    let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");
-    let mut line_positions = text.match_indices(&line_search_string);
-    let line_position = line_positions.next()?;
-
-    // The line-based search string must also appear exactly once
-    if line_positions.next().is_some() {
-        return None;
-    }
-
-    let line_symbol_start = line_position.0 + line_based_before.len();
-    let line_symbol_start_anchor =
-        snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));
-
-    Some(line_symbol_start_anchor)
-}

crates/assistant_tools/src/symbol_info_tool.rs 🔗

@@ -1,307 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_tool::{ActionLog, Tool, ToolResult};
-use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
-use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
-use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{fmt::Write, ops::Range, sync::Arc};
-use ui::IconName;
-use util::markdown::MarkdownInlineCode;
-
-use crate::schema::json_schema_for;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct SymbolInfoToolInput {
-    /// The relative path to the file containing the symbol.
-    ///
-    /// WARNING: you MUST start this path with one of the project's root directories.
-    pub path: String,
-
-    /// The information to get about the symbol.
-    pub command: Info,
-
-    /// The text that comes immediately before the symbol in the file.
-    pub context_before_symbol: String,
-
-    /// The symbol name. This text must appear in the file right between `context_before_symbol`
-    /// and `context_after_symbol`.
-    ///
-    /// The file must contain exactly one occurrence of `context_before_symbol` followed by
-    /// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
-    /// or if it contains more than one occurrence, the tool will fail, so it is absolutely
-    /// critical that you verify ahead of time that the string is unique. You can search
-    /// the file's contents to verify this ahead of time.
-    ///
-    /// To make the string more likely to be unique, include a minimum of 1 line of context
-    /// before the symbol, as well as a minimum of 1 line of context after the symbol.
-    /// If these lines of context are not enough to obtain a string that appears only once
-    /// in the file, then double the number of context lines until the string becomes unique.
-    /// (Start with 1 line before and 1 line after though, because too much context is
-    /// needlessly costly.)
-    ///
-    /// Do not alter the context lines of code in any way, and make sure to preserve all
-    /// whitespace and indentation for all lines of code. The combined string must be exactly
-    /// as it appears in the file, or else this tool call will fail.
-    pub symbol: String,
-
-    /// The text that comes immediately after the symbol in the file.
-    pub context_after_symbol: String,
-}
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Info {
-    /// Get the symbol's definition (where it's first assigned, even if it's declared elsewhere)
-    Definition,
-    /// Get the symbol's declaration (where it's first declared)
-    Declaration,
-    /// Get the symbol's implementation
-    Implementation,
-    /// Get the symbol's type definition
-    TypeDefinition,
-    /// Find all references to the symbol in the project
-    References,
-}
-
-pub struct SymbolInfoTool;
-
-impl Tool for SymbolInfoTool {
-    fn name(&self) -> String {
-        "symbol_info".into()
-    }
-
-    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
-        false
-    }
-
-    fn description(&self) -> String {
-        include_str!("./symbol_info_tool/description.md").into()
-    }
-
-    fn icon(&self) -> IconName {
-        IconName::Code
-    }
-
-    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
-        json_schema_for::<SymbolInfoToolInput>(format)
-    }
-
-    fn ui_text(&self, input: &serde_json::Value) -> String {
-        match serde_json::from_value::<SymbolInfoToolInput>(input.clone()) {
-            Ok(input) => {
-                let symbol = MarkdownInlineCode(&input.symbol);
-
-                match input.command {
-                    Info::Definition => {
-                        format!("Find definition for {symbol}")
-                    }
-                    Info::Declaration => {
-                        format!("Find declaration for {symbol}")
-                    }
-                    Info::Implementation => {
-                        format!("Find implementation for {symbol}")
-                    }
-                    Info::TypeDefinition => {
-                        format!("Find type definition for {symbol}")
-                    }
-                    Info::References => {
-                        format!("Find references for {symbol}")
-                    }
-                }
-            }
-            Err(_) => "Get symbol info".to_string(),
-        }
-    }
-
-    fn run(
-        self: Arc<Self>,
-        input: serde_json::Value,
-        _messages: &[LanguageModelRequestMessage],
-        project: Entity<Project>,
-        action_log: Entity<ActionLog>,
-        _window: Option<AnyWindowHandle>,
-        cx: &mut App,
-    ) -> ToolResult {
-        let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
-            Ok(input) => input,
-            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
-        };
-
-        cx.spawn(async move |cx| {
-            let buffer = {
-                let project_path = project.read_with(cx, |project, cx| {
-                    project
-                        .find_project_path(&input.path, cx)
-                        .context("Path not found in project")
-                })??;
-
-                project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
-            };
-
-            action_log.update(cx, |action_log, cx| {
-                action_log.track_buffer(buffer.clone(), cx);
-            })?;
-
-            let position = {
-                let Some(range) = buffer.read_with(cx, |buffer, _cx| {
-                    find_symbol_range(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
-                })? else {
-                    return Err(anyhow!(
-                        "Failed to locate the text specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
-                    ));
-                };
-
-                buffer.read_with(cx, |buffer, _| {
-                    range.start.to_point_utf16(&buffer.snapshot())
-                })?
-            };
-
-            let output: String = match input.command {
-                Info::Definition => {
-                    render_locations(project
-                        .update(cx, |project, cx| {
-                            project.definition(&buffer, position, cx)
-                        })?
-                        .await?.into_iter().map(|link| link.target),
-                        cx)
-                }
-                Info::Declaration => {
-                    render_locations(project
-                        .update(cx, |project, cx| {
-                            project.declaration(&buffer, position, cx)
-                        })?
-                        .await?.into_iter().map(|link| link.target),
-                        cx)
-                }
-                Info::Implementation => {
-                    render_locations(project
-                        .update(cx, |project, cx| {
-                            project.implementation(&buffer, position, cx)
-                        })?
-                        .await?.into_iter().map(|link| link.target),
-                        cx)
-                }
-                Info::TypeDefinition => {
-                    render_locations(project
-                        .update(cx, |project, cx| {
-                            project.type_definition(&buffer, position, cx)
-                        })?
-                        .await?.into_iter().map(|link| link.target),
-                        cx)
-                }
-                Info::References => {
-                    render_locations(project
-                        .update(cx, |project, cx| {
-                            project.references(&buffer, position, cx)
-                        })?
-                        .await?,
-                        cx)
-                }
-            };
-
-            if output.is_empty() {
-                Err(anyhow!("None found."))
-            } else {
-                Ok(output)
-            }
-        }).into()
-    }
-}
-
-/// Finds the range of the symbol in the buffer, if it appears between context_before_symbol
-/// and context_after_symbol, and if that combined string has one unique result in the buffer.
-fn find_symbol_range(
-    buffer: &Buffer,
-    context_before_symbol: &str,
-    symbol: &str,
-    context_after_symbol: &str,
-) -> Option<Range<Anchor>> {
-    let snapshot = buffer.snapshot();
-    let text = snapshot.text();
-    let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
-    let mut positions = text.match_indices(&search_string);
-    let position = positions.next()?.0;
-
-    // The combined string must appear exactly once.
-    if positions.next().is_some() {
-        return None;
-    }
-
-    let symbol_start = position + context_before_symbol.len();
-    let symbol_end = symbol_start + symbol.len();
-    let symbol_start_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
-    let symbol_end_anchor = snapshot.anchor_before(snapshot.offset_to_point(symbol_end));
-
-    Some(symbol_start_anchor..symbol_end_anchor)
-}
-
-fn render_locations(locations: impl IntoIterator<Item = Location>, cx: &mut AsyncApp) -> String {
-    let mut answer = String::new();
-
-    for location in locations {
-        location
-            .buffer
-            .read_with(cx, |buffer, _cx| {
-                if let Some(target_path) = buffer
-                    .file()
-                    .and_then(|file| file.path().as_os_str().to_str())
-                {
-                    let snapshot = buffer.snapshot();
-                    let start = location.range.start.to_point(&snapshot);
-                    let end = location.range.end.to_point(&snapshot);
-                    let start_line = start.row + 1;
-                    let start_col = start.column + 1;
-                    let end_line = end.row + 1;
-                    let end_col = end.column + 1;
-
-                    if start_line == end_line {
-                        writeln!(answer, "{target_path}:{start_line},{start_col}")
-                    } else {
-                        writeln!(
-                            answer,
-                            "{target_path}:{start_line},{start_col}-{end_line},{end_col}",
-                        )
-                    }
-                    .ok();
-
-                    write_code_excerpt(&mut answer, &snapshot, &location.range);
-                }
-            })
-            .ok();
-    }
-
-    // Trim trailing newlines without reallocating.
-    answer.truncate(answer.trim_end().len());
-
-    answer
-}
-
-fn write_code_excerpt(buf: &mut String, snapshot: &BufferSnapshot, range: &Range<Anchor>) {
-    const MAX_LINE_LEN: u32 = 200;
-
-    let start = range.start.to_point(snapshot);
-    let end = range.end.to_point(snapshot);
-
-    for row in start.row..=end.row {
-        let row_start = Point::new(row, 0);
-        let row_end = if row < snapshot.max_point().row {
-            Point::new(row + 1, 0)
-        } else {
-            Point::new(row, u32::MAX)
-        };
-
-        buf.extend(
-            snapshot
-                .text_for_range(row_start..row_end)
-                .take(MAX_LINE_LEN as usize),
-        );
-
-        if row_end.column > MAX_LINE_LEN {
-            buf.push_str("…\n");
-        }
-
-        buf.push('\n');
-    }
-}