Detailed changes
@@ -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
@@ -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));
@@ -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()
- }
-}
@@ -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)
-}
@@ -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(®ex_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) = ®ex {
- 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
- })
-}
@@ -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()
- }
- }
-}
@@ -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)
-}
@@ -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');
- }
-}