diff --git a/assets/settings/default.json b/assets/settings/default.json index 3d92f4dc27..593ec7cbbc 100644 --- a/assets/settings/default.json +++ b/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 diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 756a9271b1..9dacd422f5 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/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, 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)); diff --git a/crates/assistant_tools/src/batch_tool.rs b/crates/assistant_tools/src/batch_tool.rs deleted file mode 100644 index c8514f2f9e..0000000000 --- a/crates/assistant_tools/src/batch_tool.rs +++ /dev/null @@ -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. - /// - /// - /// 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 - /// } - /// ``` - /// - /// - /// - /// 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 - /// } - /// ``` - /// - /// - /// - /// Searching and analyzing code (concurrent) - /// - /// ```json - /// { - /// "invocations": [ - /// { - /// "name": "grep", - /// "input": { - /// "regex": "impl Database" - /// } - /// }, - /// { - /// "name": "find_path", - /// "input": { - /// "glob": "**/*test*.rs" - /// } - /// } - /// ], - /// "run_tools_concurrently": true - /// } - /// ``` - /// - /// - /// - /// 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,\n}", - /// "replace": "pub struct User {\n pub id: u64,\n pub username: String,\n pub email: String,\n pub created_at: DateTime,\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 - /// } - /// ``` - /// - pub invocations: Vec, - - /// 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::(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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(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, - input: serde_json::Value, - messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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() - } -} diff --git a/crates/assistant_tools/src/code_action_tool.rs b/crates/assistant_tools/src/code_action_tool.rs deleted file mode 100644 index c03c85de95..0000000000 --- a/crates/assistant_tools/src/code_action_tool.rs +++ /dev/null @@ -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, - - /// 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, - - /// 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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(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::(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, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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::(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::>().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> { - 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) -} diff --git a/crates/assistant_tools/src/code_symbols_tool.rs b/crates/assistant_tools/src/code_symbols_tool.rs deleted file mode 100644 index 8da5646ed0..0000000000 --- a/crates/assistant_tools/src/code_symbols_tool.rs +++ /dev/null @@ -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. - /// - /// - /// 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`. - /// - #[serde(default)] - pub path: Option, - - /// Optional regex pattern to filter symbols by name. - /// When provided, only symbols whose names match this pattern will be included in the results. - /// - /// - /// 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_". - /// - #[serde(default)] - pub regex: Option, - - /// Whether the regex is case-sensitive. Defaults to false (case-insensitive). - /// - /// - /// Set to `true` to make regex matching case-sensitive. - /// - #[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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(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, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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, - regex: Option, - offset: u32, - cx: &mut AsyncApp, -) -> anyhow::Result { - 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> = 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 - }) -} diff --git a/crates/assistant_tools/src/contents_tool.rs b/crates/assistant_tools/src/contents_tool.rs deleted file mode 100644 index 48cdaee516..0000000000 --- a/crates/assistant_tools/src/contents_tool.rs +++ /dev/null @@ -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. - /// - /// - /// 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`. - /// - 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, - - /// 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, -} - -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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(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, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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::>() - .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() - } - } -} diff --git a/crates/assistant_tools/src/rename_tool.rs b/crates/assistant_tools/src/rename_tool.rs deleted file mode 100644 index b713f31040..0000000000 --- a/crates/assistant_tools/src/rename_tool.rs +++ /dev/null @@ -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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - format!("Rename '{}' to '{}'", input.symbol, input.new_name) - } - Err(_) => "Rename symbol".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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 { - 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) -} diff --git a/crates/assistant_tools/src/symbol_info_tool.rs b/crates/assistant_tools/src/symbol_info_tool.rs deleted file mode 100644 index ade71dfbf1..0000000000 --- a/crates/assistant_tools/src/symbol_info_tool.rs +++ /dev/null @@ -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 { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(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, - input: serde_json::Value, - _messages: &[LanguageModelRequestMessage], - project: Entity, - action_log: Entity, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(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> { - 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, 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) { - 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'); - } -}